# -*- coding: utf-8 -*-
"""Pemeriksa kesiapan GPU / CUDA untuk aplikasi Vision AI (Windows/Linux).

Jalankan:  check_cuda.bat            (di folder instalasi - memakai Python bawaan aplikasi,
                                       jadi torch yang diuji = torch yang dipakai aplikasi)
      atau: python check_cuda.py     (Python apa pun >= 3.8; tanpa torch = cek driver saja)

Yang diperiksa:
  1. Driver NVIDIA & versi CUDA yang didukung driver (nvidia-smi)
  2. Build PyTorch aplikasi (CUDA berapa), torch.cuda.is_available(), uji hitung di GPU
  3. GPU: nama, VRAM, compute capability
  4. Kesimpulan LULUS/GAGAL + langkah perbaikan + tabel kebutuhan minimum

Aturan kecocokan (CUDA "minor version compatibility"): aplikasi yang dibangun dengan
CUDA 12.x berjalan pada driver apa pun yang mendukung CUDA >= 12.0. Driver yang
mendukung versi CUDA sama/lebih baru dari build aplikasi = dukungan penuh (disarankan).
Bukti paling menentukan adalah torch.cuda.is_available() + uji hitung, bukan angka driver.

Exit code: 0 = GPU siap, 1 = perlu perbaikan / jalan di CPU, 2 = tidak ada GPU NVIDIA.
"""
import os
import platform
import re
import shutil
import subprocess
import sys

APP_TORCH_CUDA = "12.6"        # build CUDA PyTorch yang dibundel aplikasi (dibaca dari torch bila ada)
MIN_CUDA_DRIVER = "12.0"       # minimum absolut (minor version compatibility)
MIN_DRIVER = {                 # driver minimum utk CUDA tsb: (Windows, Linux) - tabel resmi NVIDIA
    "12.0": ("527.41", "525.60"),
    "12.6": ("560.76", "560.28"),
}
MIN_CC = (5, 0)                # compute capability minimum (Maxwell) utk wheel PyTorch cu12x
MIN_VRAM_GB = 4                # inferensi; training model small/medium disarankan >= 6 GB
IS_WIN = platform.system() == "Windows"


def vtuple(v):
    return tuple(int(x) for x in re.findall(r"\d+", str(v))[:3])


def run(cmd, timeout=15):
    try:
        return subprocess.run(cmd, capture_output=True, text=True, timeout=timeout).stdout
    except Exception:
        return ""


def section(title):
    print("\n" + "=" * 66)
    print(title)
    print("=" * 66)


def main():
    print("Pemeriksa CUDA / GPU  -  " + platform.platform())
    print("Python " + sys.version.split()[0] + "  (" + sys.executable + ")")
    problems, notes = [], []

    # ---------------------------------------------------------------- 1) driver
    section("1) Driver NVIDIA")
    smi = shutil.which("nvidia-smi") or (r"C:\Windows\System32\nvidia-smi.exe" if IS_WIN else "")
    driver = cuda_drv = ""
    gpus = []
    if smi and os.path.exists(smi):
        head = run([smi])
        m = re.search(r"Driver Version:\s*([\d.]+)", head)
        driver = m.group(1) if m else ""
        m = re.search(r"CUDA Version:\s*([\d.]+)", head)
        cuda_drv = m.group(1) if m else ""
        q = run([smi, "--query-gpu=name,memory.total,compute_cap", "--format=csv,noheader"])
        for line in q.splitlines():
            parts = [p.strip() for p in line.split(",")]
            if len(parts) >= 2:
                gpus.append({"name": parts[0], "vram": parts[1], "cc": parts[2] if len(parts) > 2 else ""})
        if not driver:
            q2 = run([smi, "--query-gpu=driver_version", "--format=csv,noheader"])
            driver = q2.strip().splitlines()[0] if q2.strip() else ""
    if not driver:
        print("  nvidia-smi tidak ditemukan / tidak ada GPU NVIDIA yang dikenali driver.")
        print("  -> Pasang driver NVIDIA terbaru dari https://www.nvidia.com/drivers lalu restart.")
    else:
        print(f"  Driver         : {driver}")
        print(f"  CUDA (driver)  : {cuda_drv or '?'}   <- versi CUDA tertinggi yang didukung driver ini")
        for g in gpus:
            print(f"  GPU            : {g['name']}  |  VRAM {g['vram']}  |  compute capability {g['cc'] or '?'}")
    cv = os.environ.get("CUDA_VISIBLE_DEVICES")
    if cv is not None:
        print(f"  CUDA_VISIBLE_DEVICES = {cv!r}")
        if cv.strip() in ("", "-1"):
            problems.append("Variabel lingkungan CUDA_VISIBLE_DEVICES menyembunyikan GPU - hapus variabel itu")

    # ---------------------------------------------------------------- 2) torch
    section("2) PyTorch aplikasi")
    torch_cuda = APP_TORCH_CUDA
    torch_ok = None            # None = torch tidak ada di Python ini
    torch_err = ""
    try:
        import torch  # noqa
        torch_cuda = torch.version.cuda or APP_TORCH_CUDA
        print(f"  torch          : {torch.__version__}  (build CUDA {torch_cuda})")
        try:
            torch_ok = bool(torch.cuda.is_available())
        except Exception as e:
            torch_ok, torch_err = False, str(e)
        print(f"  cuda available : {torch_ok}")
        if torch_ok:
            for i in range(torch.cuda.device_count()):
                p = torch.cuda.get_device_properties(i)
                print(f"  device {i}       : {p.name}  |  {p.total_memory / 1e9:.1f} GB  |  sm_{p.major}{p.minor}")
                if (p.major, p.minor) < MIN_CC:
                    problems.append(f"GPU {p.name} compute capability {p.major}.{p.minor} < {MIN_CC[0]}.{MIN_CC[1]}")
            try:
                x = torch.ones(64, 64, device="cuda") @ torch.ones(64, 64, device="cuda")
                torch.cuda.synchronize()
                print(f"  uji hitung GPU : OK (matmul 64x64 = {float(x[0, 0]):.0f})")
            except Exception as e:
                torch_ok, torch_err = False, str(e)
                print(f"  uji hitung GPU : GAGAL: {str(e)[:160]}")
        else:
            # gali alasannya: driver terlalu lama / DLL CUDA tidak termuat / tidak ada device
            try:
                torch.cuda.init()
            except Exception as e:
                torch_err = torch_err or str(e)
            if torch_err:
                print(f"  alasan torch   : {torch_err[:220]}")
    except ImportError:
        print(f"  torch tidak ada di Python ini -> memakai asumsi build aplikasi: CUDA {APP_TORCH_CUDA}")
        print("  (jalankan lewat check_cuda.bat di folder instalasi untuk menguji torch yang sebenarnya)")

    # ---------------------------------------------------------------- 3) kecocokan
    section("3) Kecocokan driver vs build CUDA aplikasi")
    need_min = MIN_DRIVER["12.0"][0 if IS_WIN else 1]
    need_full = MIN_DRIVER["12.6"][0 if IS_WIN else 1]
    if driver:
        if cuda_drv and vtuple(cuda_drv) < vtuple(MIN_CUDA_DRIVER):
            print(f"  GAGAL: driver hanya mendukung CUDA {cuda_drv}; aplikasi (CUDA {torch_cuda}) butuh driver dengan CUDA >= {MIN_CUDA_DRIVER}.")
            print(f"         Minimal driver {need_min}, disarankan >= {need_full}. Terpasang: {driver}.")
            problems.append(f"Driver {driver} (CUDA {cuda_drv}) terlalu lama: butuh CUDA >= {MIN_CUDA_DRIVER} (driver >= {need_min})")
        elif cuda_drv and vtuple(cuda_drv) < vtuple(torch_cuda):
            print(f"  OK (kompatibilitas minor): driver mendukung CUDA {cuda_drv}, aplikasi dibangun dengan CUDA {torch_cuda}.")
            print(f"     Berjalan lewat CUDA minor-version compatibility. Untuk dukungan penuh update driver ke >= {need_full}.")
            notes.append(f"Driver {driver} (CUDA {cuda_drv}) masih di bawah build aplikasi CUDA {torch_cuda}; disarankan update ke >= {need_full}")
        else:
            print(f"  OK: driver {driver} mendukung CUDA {cuda_drv or '?'} >= build aplikasi {torch_cuda}.")
    for g in gpus:
        m = re.search(r"(\d+)", g["vram"])
        if m and int(m.group(1)) < MIN_VRAM_GB * 1024:
            notes.append(f"VRAM {g['vram']} < {MIN_VRAM_GB} GB: inferensi mungkin jalan, training model besar akan gagal")
        if g["cc"]:
            try:
                if vtuple(g["cc"]) < MIN_CC:
                    problems.append(f"Compute capability {g['cc']} < {MIN_CC[0]}.{MIN_CC[1]} (GPU terlalu lama untuk PyTorch cu12)")
            except Exception:
                pass
    if driver and torch_ok is False and not any("terlalu lama" in p for p in problems):
        hint = ("torch.cuda.is_available() = False padahal driver cukup. Penyebab umum: (1) aplikasi/servis "
                "dimulai sebelum driver siap -> restart PC lalu jalankan lagi; (2) DLL CUDA aplikasi gagal dimuat "
                "-> pasang 'Microsoft Visual C++ Redistributable 2015-2022 x64'; (3) GPU dinonaktifkan di Device Manager; "
                "(4) driver rusak -> pasang ulang driver dengan opsi Clean Install")
        if torch_err:
            hint += f" | pesan torch: {torch_err[:160]}"
        problems.append(hint)

    # ---------------------------------------------------------------- 4) kesimpulan
    section("4) Kesimpulan")
    if not driver:
        code = 2
        print("  TIDAK ADA GPU NVIDIA -> aplikasi jalan di CPU (lebih lambat, tetap berfungsi).")
    elif problems:
        code = 1
        print("  GPU BELUM BISA DIPAKAI aplikasi. Masalah:")
        for p in problems:
            print("   - " + p)
        print("  Langkah umum: update driver NVIDIA (Game Ready / Studio) ke versi terbaru dari")
        print("                https://www.nvidia.com/drivers , restart PC, jalankan pemeriksaan ini lagi.")
    elif torch_ok:
        code = 0
        print("  LULUS: GPU siap dan TERBUKTI dipakai PyTorch aplikasi (inferensi & training di CUDA).")
    else:
        code = 0
        print("  LULUS (driver): driver & GPU memenuhi syarat. Uji torch belum dijalankan - jalankan")
        print("  check_cuda.bat dari folder instalasi untuk pembuktian penuh.")
    for n in notes:
        print("  Catatan: " + n)

    section("Kebutuhan minimum aplikasi (GPU opsional; tanpa GPU = CPU)")
    print(f"  OS             : Windows 10/11 64-bit (build 19045+) atau Ubuntu 20.04+/JetPack 6")
    print(f"  GPU            : NVIDIA compute capability >= {MIN_CC[0]}.{MIN_CC[1]} (GTX 900 series / Maxwell ke atas)")
    print(f"  VRAM           : >= {MIN_VRAM_GB} GB inferensi, >= 6 GB training (8 GB+ disarankan)")
    print(f"  Driver NVIDIA  : minimum {MIN_DRIVER['12.0'][0]} Win / {MIN_DRIVER['12.0'][1]} Linux (CUDA >= {MIN_CUDA_DRIVER});")
    print(f"                   disarankan >= {MIN_DRIVER['12.6'][0]} Win / {MIN_DRIVER['12.6'][1]} Linux (CUDA {torch_cuda}, dukungan penuh)")
    print(f"  CUDA toolkit   : TIDAK perlu dipasang terpisah - runtime CUDA {torch_cuda} sudah dibundel aplikasi")
    print(f"  RAM            : >= 16 GB (8 GB minimum, hanya inferensi)")
    print(f"  Disk           : >= 12 GB bebas untuk instalasi + ruang data project")
    print(f"  CPU tanpa GPU  : tetap jalan; inferensi ~5-20x lebih lambat, training tidak disarankan")
    return code


if __name__ == "__main__":
    try:
        sys.exit(main())
    except KeyboardInterrupt:
        sys.exit(1)
