#!/usr/bin/env python3
# SkillFishOS Tuner — KDE-native (PyQt6/Qt Widgets). Themed by Kvantum/Breeze.
# The GUI talks to the root daemon skillfish-tuner-helper (pkexec, one auth at start).
# Interfaccia in IT/EN/PL/UK, scelta dal locale (LANG/LC_*).
import sys, os, json, subprocess, re, collections, time
from PyQt6.QtCore import Qt, QThread, pyqtSignal, QTimer, QRect, QSize
from PyQt6.QtGui import QIcon, QPainter, QColor, QPen, QBrush, QLinearGradient, QPainterPath, QFont
from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
                             QLabel, QGroupBox, QSlider, QPushButton, QToolButton, QCheckBox,
                             QComboBox, QScrollArea, QMessageBox, QFrame, QGridLayout, QButtonGroup,
    QRadioButton,
                             QLayout, QDialog, QPlainTextEdit)

HELPER  = "/usr/local/bin/skillfish-tuner-helper"

# Il pallino «?» condiviso. ⚠️ Arriva da skillfish-base, che e' un pacchetto
# diverso: se manca (macchina aggiornata a meta') si ricade sul bottone di
# prima, che fa la stessa cosa con l'aspetto di sistema.
sys.path.insert(0, "/usr/share/skillfish")
try:
    from aiuto import Aiuto
except Exception:
    class Aiuto(QToolButton):
        def __init__(self, testo, titolo=None, parent=None):
            super().__init__(parent)
            self.setText("?")
            self.setToolTip(titolo or "")
            self.clicked.connect(
                lambda: QMessageBox.information(self.window(), titolo or "?", testo))
PRESETS = "/usr/share/skillfish/tuner-presets.json"
ICON    = "/usr/share/icons/hicolor/256x256/apps/skillfish-tuner.png"


# ---------- i18n ----------
def _detect_lang():
    # Ordine standard dei locale: LC_ALL batte LC_MESSAGES batte LANG;
    # LANGUAGE (la lista con i due punti di gettext) e' l'ultimo ripiego.
    val = (os.environ.get("LC_ALL") or os.environ.get("LC_MESSAGES")
           or os.environ.get("LANG") or os.environ.get("LANGUAGE") or "")
    v = val.lower()
    if v.startswith("it"): return "it"
    if v.startswith("pl"): return "pl"
    # "uk" e' il codice ISO 639-1 della lingua ucraina. "ua" e' il codice del
    # PAESE e non dovrebbe comparire qui, ma nei locale scritti a mano capita.
    if v.startswith("uk") or v.startswith("ua"): return "uk"
    # Qualsiasi altra lingua: si restituisce il codice a due lettere e sara' il
    # dizionario condiviso (/usr/share/skillfish/i18n/<lingua>.json) a servirla.
    # ⚠️ Prima qui c'era "return en" secco: un elenco chiuso di tre lingue. Con
    # quello, aggiungere una traduzione nuova non aveva alcun effetto - la
    # lingua veniva scartata prima di arrivare al dizionario.
    codice = v.replace("-", "_").split(".")[0].split("@")[0].split("_")[0].strip()
    if len(codice) == 2 and codice.isalpha():
        return codice
    return "en"

LANG = _detect_lang()

# --- dizionario condiviso per le lingue nuove -------------------------------
# ⚠️ Non deve mai impedire alla app di partire: se il modulo o il file della
# lingua mancano, si resta all'inglese. Una traduzione assente e' un fastidio,
# una app che non si apre e' un guasto.
try:
    sys.path.insert(0, "/usr/share/skillfish")
    from i18n import traduttore as _traduttore
    _TR = _traduttore(LANG)
except Exception:
    def _TR(en):
        return en


# Traduzione polacca di Cyryl Sochacki (github.com/cyryllo).
# Le chiavi sono le stringhe INGLESI passate a L(): cosi' L(it, en) mantiene la
# stessa firma e tutte le chiamate esistenti restano valide. Una stringa non
# ancora tradotta ricade sull'inglese invece di sparire.
PL = {
    # stringhe con segnaposto (il valore entra dopo la traduzione)
    " · %d GPU errors": " · %d błędów GPU",
    "(%d MHz @ %d failed the test and was rolled back.)": "(%d MHz @ %d nie przeszło testu i zostało cofnięte.)",
    "(%d MHz failed the test and was rolled back.)": "(%d MHz nie przeszło testu i zostało cofnięte.)",
    "CPU wizard: testing %d MHz @ %d…": "Kreator CPU: test %d MHz @ %d…",
    "Current allocation: %d MB (%.1f GB)": "Bieżący przydział: %d MB (%.1f GB)",
    "Highest load-stable CPU point: %d MHz @ undervolt %d (already applied and saved at boot).": "Najwyższy punkt CPU stabilny pod obciążeniem: %d MHz @ undervolt %d (już zastosowany i zapisywany przy starcie).",
    "Preset «%s» applied": "Zastosowano profil «%s»",
    "Searching the optimal undervolt for %s MHz (may take ~1 min)…": "Szukanie optymalnego undervoltu dla %s MHz (może potrwać ~1 min)…",
    "Suggested undervolt for %s MHz: scale %s (set, press Apply)": "Sugerowany undervolt dla %s MHz: skala %s (ustawiony, naciśnij Zastosuj)",
    "VRAM %s MB — reboot": "VRAM %s MB — ponowne uruchomienie",
    "VRAM to %s MB?": "Ustawić VRAM na %s MB?",
    "Wizard: testing %d MHz…": "Kreator: test %d MHz…",
    "“%s” is meant ONLY for benchmark sessions with very good cooling.\n\nIt stays applied across reboots and on some boards causes sudden total freezes (even at idle). Switch back to Turbo or Performance after benchmarking.\n\nApply anyway?": "„%s” jest przeznaczony WYŁĄCZNIE do sesji benchmarkowych z bardzo dobrym chłodzeniem.\n\nPozostaje aktywny po ponownym uruchomieniu, a na niektórych płytach powoduje nagłe całkowite zawieszenia (nawet na biegu jałowym). Po benchmarku wróć do Turbo lub Wydajność.\n\nZastosować mimo to?",
    "✓ %d MHz @ %d stable": "✓ %d MHz @ %d stabilne",
    "✓ %d MHz stable": "✓ %d MHz stabilne",
    "✓ CPU OK and applied — %s %s, %s°C": "✓ CPU OK i zastosowane — %s %s, %s°C",
    "✓ GPU OK and applied — %s %s, %s°C": "✓ GPU OK i zastosowane — %s %s, %s°C",

    "Max CPU frequency": "Maksymalna częstotliwość CPU",
    "CPU boost limit in MHz.\n\n• 3500 = safe base\n• 3700 = verified stable on this chip\n• 3800-3900 = beyond limits, possible thermal throttling\n\nVoltage (Vid) always stays below 1.325 V for safety.": "Limit boostu CPU w MHz.\n\n• 3500 = bezpieczna baza\n• 3700 = zweryfikowana stabilność na tym układzie\n• 3800-3900 = poza limitami, możliwy throttling termiczny\n\nNapięcie (Vid) zawsze pozostaje poniżej 1,325 V dla bezpieczeństwa.",
    "Undervolt (mV)": "Undervolt (skala)",
    "Lowers the CPU voltage below the factory curve.\n\n• 0 mV = no undervolt (default)\n• the SMU moves in steps of 6.25 mV, so the value jumps by 6 or 7\n\nLess voltage means lower temperatures and less power, but going too far makes the system unstable. «Suggest UV» proposes a value, «Test (benchmark)» verifies it.": "Obniża napięcie CPU poniżej fabrycznej krzywej.\n\n• 0 mV = brak undervoltu (domyślnie)\n• SMU zmienia napięcie skokami po 6,25 mV, więc wartość skacze o 6 lub 7\n\nMniejsze napięcie to niższe temperatury i mniejszy pobór mocy, ale przesada powoduje niestabilność. «Zasugeruj UV» proponuje wartość, «Test (benchmark)» ją sprawdza.",
    "CPU temperature limit": "Limit temperatury CPU",
    "Thermal threshold above which the SMU throttles frequency.\n\nRecommended 85 °C. Above it, the system automatically lowers the clock to protect the hardware.": "Próg termiczny, powyżej którego SMU ogranicza częstotliwość.\n\nZalecane 85 °C. Powyżej tej wartości system automatycznie obniża zegar, aby chronić sprzęt.",
    "Max GPU frequency": "Maksymalna częstotliwość GPU",
    "Maximum GPU clock (SMU governor).\n\n• 1500 = power saving\n• 2000 = balanced (safe on every board)\n• 2200 = max stable at 1000 mV on stock cooling\n\nThe GPU reaches this frequency only under load, then returns to 350 MHz at idle. Above 2200 MHz at 1000 mV the BC-250 can hard-freeze: it needs more voltage and depends on the silicon lottery.": "Maksymalny zegar GPU (governor SMU).\n\n• 1500 = oszczędzanie energii\n• 2000 = zbalansowany (bezpieczny na każdej płycie)\n• 2200 = maksimum stabilne przy 1000 mV na chłodzeniu fabrycznym\n\nGPU osiąga tę częstotliwość tylko pod obciążeniem, a w spoczynku wraca do 350 MHz. Powyżej 2200 MHz przy 1000 mV BC-250 może się całkowicie zawiesić: potrzeba wyższego napięcia, a wynik zależy od loterii krzemowej.",
    "Max GPU voltage": "Maksymalne napięcie GPU",
    "Voltage tied to the max GPU frequency (mV).\n\nHigher = more stable at high frequency but hotter. Range 700-1129 mV.": "Napięcie powiązane z maksymalną częstotliwością GPU (mV).\n\nWyższe = większa stabilność przy wysokiej częstotliwości, ale wyższa temperatura. Zakres 700-1129 mV.",
    "GPU governor mode": "Tryb governora GPU",
    "How the governor picks the GPU clock under load:\n\n• Balanced: raises the clock only as much as needed — cooler and quieter (default).\n• Performance: holds the GPU at max clock under any gaming load (best FPS in GPU-bound games, more heat and power). It still returns to 350 MHz at idle.\n\nBoth modes use the governor's safe SMU path.": "Jak governor dobiera zegar GPU pod obciążeniem:\n\n• Zbalansowany: podnosi zegar tylko na tyle, na ile potrzeba — chłodniej i ciszej (domyślnie).\n• Performance: utrzymuje GPU na maksymalnym zegarze pod każdym obciążeniem w grach (najlepsze FPS w grach ograniczanych przez GPU, więcej ciepła i poboru mocy). W spoczynku i tak wraca do 350 MHz.\n\nOba tryby korzystają z bezpiecznej ścieżki SMU governora.",
    "VRAM (graphics memory)": "VRAM (pamięć graficzna)",
    "Tuner log": "Dziennik Tunera",
    "Refresh": "Odśwież",
    "Copy": "Kopiuj",
    "Close": "Zamknij",
    "Copied to the clipboard.": "Skopiowano do schowka.",
    "Amount of system RAM reserved for the GPU (UMA).\n\n⚠ Writes to the BIOS CMOS and requires a REBOOT to apply. Reversible by clearing the CMOS (jumper on the board).\n\nDon't exceed values that leave too little RAM for the system.": "Ilość pamięci RAM systemu zarezerwowanej dla GPU (UMA).\n\n⚠ Zapisuje w CMOS-ie BIOS-u i wymaga PONOWNEGO URUCHOMIENIA, aby zastosować zmiany. Odwracalne przez wyzerowanie CMOS-u (zworka na płycie).\n\nNie przekraczaj wartości, które zostawiają systemowi zbyt mało RAM-u.",
    "Compute Units (CU)": "Jednostki obliczeniowe (CU)",
    "CUs are the GPU's compute cores. Here you enable/disable them LIVE, no reboot.\n\n• Each square = 1 CU: GREEN = active, RED = off.\n• CLICK a pair to toggle it manually (pairs are managed 2 CU at a time = 1 WGP).\n• Or use the PRESETS (24 / 32 / 40 CU).\n• The first 3 pairs per row (24 CU) are the driver minimum and stay always on.\n• Hit «Apply live» to make the choice effective.\n\nMore CU = more graphics power but also more heat and power draw.\n\nUse «Test CU» to verify the active CUs are stable and defect-free (silicon lottery).": "CU to rdzenie obliczeniowe GPU. Tutaj włączasz/wyłączasz je NA ŻYWO, bez restartu.\n\n• Każdy kwadracik = 1 CU: ZIELONY = aktywna, CZERWONY = wyłączona.\n• KLIKNIJ parę, aby przełączyć ją ręcznie (pary zarządzane są po 2 CU naraz = 1 WGP).\n• Albo użyj PRESETÓW (24 / 32 / 40 CU).\n• Pierwsze 3 pary w każdym rzędzie (24 CU) to minimum sterownika i zawsze pozostają aktywne.\n• Naciśnij «Zastosuj na żywo», aby uaktywnić wybór.\n\nWięcej CU = więcej mocy graficznej, ale też więcej ciepła i poboru energii.\n\nUżyj «Test CU», aby sprawdzić, czy aktywne CU są stabilne i wolne od defektów (loteria krzemowa).",
    "daemon not running": "demon nie działa",
    "locked (always on)": "zablokowana (zawsze aktywna)",
    "on": "aktywna",
    "off": "wyłączona",
    "◉  LIVE MONITOR": "◉  MONITOR NA ŻYWO",
    "Temperature (°C)": "Temperatura (°C)",
    "Frequency (MHz)": "Częstotliwość (MHz)",
    "GPU voltage (mV)": "Napięcie GPU (mV)",
    "SkillFishOS — Monitor · %s": "SkillFishOS — Monitor · %s",
    "Help": "Pomoc",
    "Presets": "Presety",
    "Apply": "Zastosuj",
    "Extreme preset": "Ekstremalny preset",
    "My silicon": "Mój krzem",
    "CPU %d MHz @ %d mV  ·  GPU %d MHz @ %d mV  ·  Active CUs: %d  ·  Detected freezes: %d": "CPU %d MHz @ %d mV  ·  GPU %d MHz @ %d mV  ·  Aktywne CU: %d  ·  Wykryte zawieszenia: %d",
    "⚠ Last freeze: %s — if it happens again, step down one notch with the wizards.": "⚠ Ostatnie zawieszenie: %s — jeśli powtórzy się, zejdź o jeden poziom niżej za pomocą kreatorów.",
    "Share to the silicon DB": "Udostępnij w bazie krzemu",
    "Max frequency": "Maksymalna częstotliwość",
    "Undervolt": "Undervolt",
    "Temperature limit": "Limit temperatury",
    "Find my max": "Znajdź maksimum",
    "Suggest UV": "Zasugeruj UV",
    "Test (benchmark)": "Test (benchmark)",
    "CPU applied": "CPU zastosowane",
    "Save at boot": "Zapisz przy starcie",
    "CPU saved at boot": "CPU zapisane przy starcie",
    "CPU test: applying and benchmarking 60s (for a realistic temp)…": "Test CPU: stosuję ustawienia i uruchamiam benchmark 60 s (dla realistycznej temperatury)…",
    "test failed": "test nieudany",
    "Max voltage": "Maksymalne napięcie",
    "Balanced": "Zbalansowany",
    "Performance": "Performance",
    "Governor: Balanced": "Governor: Zbalansowany",
    "Governor: Performance — 2230 MHz under load": "Governor: Performance — 2230 MHz pod obciążeniem",
    "Governor mode": "Tryb governora",
    "GPU applied": "GPU zastosowane",
    "The wizard steps the GPU up (2000 → 2200 MHz at 1000 mV), validating each step with a benchmark and rolling back if unstable.\n\n~2 minutes per step under full GPU load.\nWhen done it applies the highest stable point for YOUR board.\n\nProceed?": "Kreator zwiększa zegar GPU krok po kroku (2000 → 2200 MHz przy 1000 mV), sprawdzając każdy krok benchmarkiem i cofając się w razie niestabilności.\n\n~2 minuty na krok przy pełnym obciążeniu GPU.\nPo zakończeniu zastosuje najwyższy stabilny punkt dla TWOJEJ płyty.\n\nKontynuować?",
    "✗ Not even 2000 MHz is stable on this board: keeping the current profile.": "✗ Nawet 2000 MHz nie jest stabilne na tej płycie: zachowuję bieżący profil.",
    "Highest stable point: %d MHz @ 1000 mV%s (already applied).": "Najwyższy stabilny punkt: %d MHz @ 1000 mV%s (już zastosowano).",
    "Share the result (anonymous) in the silicon-lottery database on GitHub?": "Udostępnić wynik (anonimowo) w bazie loterii krzemowej na GitHubie?",
    "Wizard result": "Wynik kreatora",
    "Find my max (CPU)": "Znajdź maksimum (CPU)",
    "The wizard steps the CPU up (3600 → 4000 MHz with progressive undervolt), validating each step with a benchmark and stopping at the first unstable one.\n\n⚠ The last step can freeze some boards: the watchdog then reboots automatically and the last stable value is restored at boot.\n\nNote: the test validates UNDER-LOAD stability; idle stability over days is confirmed by regular use.\n\nProceed?": "Kreator zwiększa zegar CPU krok po kroku (3600 → 4000 MHz z progresywnym undervoltem), sprawdzając każdy krok benchmarkiem i zatrzymując się na pierwszym niestabilnym.\n\n⚠ Ostatni krok może zawiesić niektóre płyty: watchdog wtedy automatycznie uruchamia system ponownie, a przy starcie przywracana jest ostatnia stabilna wartość.\n\nUwaga: test sprawdza stabilność POD OBCIĄŻENIEM; stabilność w spoczynku przez kilka dni potwierdza dopiero codzienne użytkowanie.\n\nKontynuować?",
    "✗ Not even 3600 MHz is stable: keeping the current profile.": "✗ Nawet 3600 MHz nie jest stabilne: zachowuję bieżący profil.",
    "Tip: run the system for a few days; if a freeze appears, step down one notch.\n\nShare the result (anonymous) in the silicon-lottery database?": "Wskazówka: używaj systemu przez kilka dni; jeśli pojawi się zawieszenie, zejdź o jeden poziom niżej.\n\nUdostępnić wynik (anonimowo) w bazie loterii krzemowej?",
    "CPU wizard result": "Wynik kreatora CPU",
    "GPU test: applying and running vkpeak…": "Test GPU: stosuję ustawienia i uruchamiam vkpeak…",
    "Compute Units (CU) — live": "Jednostki obliczeniowe (CU) — na żywo",
    "40 CU (max)": "40 CU (maks.)",
    "Test CU": "Test CU",
    "Active CUs: %d / 40": "Aktywne CU: %d / 40",
    "CUs applied: %s/40 (live)": "CU zastosowane: %s/40 (na żywo)",
    "CU error: %s": "Błąd CU: %s",
    "CU test": "Test CU",
    "Tests the extra CUs (WGP3-4) one by one to catch defects (silicon lottery): enables each pair alone, stresses it with vkpeak and checks for GPU errors/hangs.\n\nTakes ~2-3 minutes and the GPU will be under load. Proceed?": "Testuje dodatkowe CU (WGP3-4) pojedynczo, aby wykryć defekty (loteria krzemowa): włącza każdą parę osobno, obciąża ją vkpeak i sprawdza błędy/zawieszenia GPU.\n\nZajmuje ~2-3 minuty, a GPU będzie pod obciążeniem. Kontynuować?",
    "CU test running (~2-3 min)…": "Test CU w toku (~2-3 min)…",
    "CU test failed: %s": "Test CU nieudany: %s",
    "40 CU under load (vkpeak): %s GFLOPS%s": "40 CU pod obciążeniem (vkpeak): %s GFLOPS%s",
    "Baseline 24 CU: %s GFLOPS": "Bazowo 24 CU: %s GFLOPS",
    "(expected ~11300 at 40 CU)": "(oczekiwane ~11300 przy 40 CU)",
    "Per extra-CU-pair stability:": "Stabilność wg pary dodatkowych CU:",
    "⚠ GPU problems under load detected: possible defective CU (silicon lottery). Consider keeping fewer CUs.": "⚠ Wykryto problemy GPU pod obciążeniem: możliwa wadliwa CU (loteria krzemowa). Rozważ pozostawienie mniejszej liczby CU.",
    "✓ No defects: all CUs sustain the load with no GPU errors.": "✓ Brak defektów: wszystkie CU wytrzymują obciążenie bez błędów GPU.",
    "CU test result": "Wynik testu CU",
    "Manual control (off = automatic curve)": "Kontrola ręczna (wyłączone = krzywa automatyczna)",
    "Speed": "Prędkość",
    "Test": "Test",
    "New allocation": "Nowy przydział",
    "Set (reboot)": "Ustaw (restart)",
    "Writes to the BIOS CMOS. A reboot is needed to apply.": "Zapisuje w CMOS-ie BIOS-u. Aby zastosować, wymagany jest restart.",
    "Authentication cancelled or helper not started.\nRestart the program and authorize the operation.": "Uwierzytelnienie anulowane lub pomocnik nie został uruchomiony.\nUruchom program ponownie i autoryzuj operację.",
    # pannello core CPU: mancava nella versione da cui e' stata
    # fatta la traduzione, queste stringhe sono mie
    "CPU cores": "Rdzenie CPU",
    "Turn CPU cores on and off LIVE, no reboot — just like the GPU's CUs.\n\n• Each box = 1 physical core with its 2 threads: GREEN = on, RED = off.\n• CLICK a core to toggle it, then «Apply».\n• Core 0 is FIXED GREEN: it holds the boot CPU and the kernel cannot offline it.\n• «SMT» toggles multi-threading: 2 threads per core, or 1.\n\nWhy: switching cores off frees thermal and power budget so the remaining cores can clock higher — handy for games that use few threads. Fewer cores = less heat, but less multi-threaded power.\n\nThe choice is NOT permanent: everything comes back on at reboot.":
        "Włączaj i wyłączaj rdzenie CPU NA ŻYWO, bez restartu — tak jak jednostki CU w GPU.\n\n• Każdy kwadrat = 1 rdzeń fizyczny z jego 2 wątkami: ZIELONY = włączony, CZERWONY = wyłączony.\n• KLIKNIJ rdzeń, aby go przełączyć, a potem «Zastosuj».\n• Rdzeń 0 jest ZAWSZE ZIELONY: obsługuje procesor rozruchowy i jądro nie może go wyłączyć.\n• «SMT» przełącza wielowątkowość: 2 wątki na rdzeń albo 1.\n\nPo co: wyłączenie rdzeni zwalnia budżet cieplny i energetyczny, dzięki czemu pozostałe rdzenie mogą osiągnąć wyższe taktowanie — przydatne w grach używających niewielu wątków. Mniej rdzeni = mniej ciepła, ale mniejsza moc w zadaniach wielowątkowych.\n\nWybór NIE jest trwały: po restarcie wszystkie wracają.",
    "CPU cores — live": "Rdzenie CPU — na żywo",
    "Multi-threading: 2 threads per core. Off = 1 thread per core.":
        "Wielowątkowość: 2 wątki na rdzeń. Wyłączone = 1 wątek na rdzeń.",
    "boot core (always on)": "rdzeń rozruchowy (zawsze włączony)",
    "All": "Wszystkie",
    "Active cores: %d / %d  ·  %d threads": "Aktywne rdzenie: %d / %d  ·  %d wątków",
    "SMT %s — %d threads": "SMT %s — %d wątków",
    "SMT: %s": "SMT: %s",
    "Cores applied — %d active threads": "Zastosowano rdzenie — %d aktywnych wątków",
    "Core error: %s": "Błąd rdzenia: %s",
}

# Traduzione ucraina. NON e' opera di un madrelingua: l'ho scritta io e va
# rivista da chi parla ucraino, come il polacco e' stato rivisto da cyryllo.
UK = {
    # stringhe con segnaposto (il valore entra dopo la traduzione)
    " · %d GPU errors": " · %d помилок ГП",
    "(%d MHz @ %d failed the test and was rolled back.)": "(%d МГц @ %d не пройшло перевірку і було скасовано.)",
    "(%d MHz failed the test and was rolled back.)": "(%d МГц не пройшло перевірку і було скасовано.)",
    "CPU wizard: testing %d MHz @ %d…": "Майстер ЦП: перевірка %d МГц @ %d…",
    "Current allocation: %d MB (%.1f GB)": "Поточний обсяг: %d МБ (%.1f ГБ)",
    "Highest load-stable CPU point: %d MHz @ undervolt %d (already applied and saved at boot).": "Найвища стабільна під навантаженням точка ЦП: %d МГц @ undervolt %d (уже застосовано і зберігається при завантаженні).",
    "Preset «%s» applied": "Застосовано профіль «%s»",
    "Searching the optimal undervolt for %s MHz (may take ~1 min)…": "Пошук оптимального undervolt для %s МГц (може зайняти ~1 хв)…",
    "Suggested undervolt for %s MHz: scale %s (set, press Apply)": "Рекомендований undervolt для %s МГц: шкала %s (встановлено, натисніть Застосувати)",
    "VRAM %s MB — reboot": "VRAM %s МБ — перезавантаження",
    "VRAM to %s MB?": "Встановити VRAM на %s МБ?",
    "Wizard: testing %d MHz…": "Майстер: перевірка %d МГц…",
    "“%s” is meant ONLY for benchmark sessions with very good cooling.\n\nIt stays applied across reboots and on some boards causes sudden total freezes (even at idle). Switch back to Turbo or Performance after benchmarking.\n\nApply anyway?": "«%s» призначено ВИКЛЮЧНО для тестових сесій з дуже добрим охолодженням.\n\nПрофіль зберігається після перезавантаження, а на деяких платах спричиняє раптові повні зависання (навіть у простої). Після тестів поверніться до Turbo або Продуктивність.\n\nВсе одно застосувати?",
    "✓ %d MHz @ %d stable": "✓ %d МГц @ %d стабільно",
    "✓ %d MHz stable": "✓ %d МГц стабільно",
    "✓ CPU OK and applied — %s %s, %s°C": "✓ ЦП OK і застосовано — %s %s, %s°C",
    "✓ GPU OK and applied — %s %s, %s°C": "✓ ГП OK і застосовано — %s %s, %s°C",

    "Max CPU frequency": "Максимальна частота ЦП",
    "CPU boost limit in MHz.\n\n• 3500 = safe base\n• 3700 = verified stable on this chip\n• 3800-3900 = beyond limits, possible thermal throttling\n\nVoltage (Vid) always stays below 1.325 V for safety.":
        "Обмеження буст-частоти ЦП у МГц.\n\n• 3500 = безпечна база\n• 3700 = перевірено стабільно на цьому чипі\n• 3800-3900 = за межами норми, можливий тепловий тротлінг\n\nНапруга (Vid) задля безпеки завжди залишається нижче 1,325 В.",
    "Undervolt (mV)": "Зниження напруги (шкала)",
    "Lowers the CPU voltage below the factory curve.\n\n• 0 mV = no undervolt (default)\n• the SMU moves in steps of 6.25 mV, so the value jumps by 6 or 7\n\nLess voltage means lower temperatures and less power, but going too far makes the system unstable. «Suggest UV» proposes a value, «Test (benchmark)» verifies it.":
        "Знижує напругу процесора відносно заводської кривої.\n\n• 0 мВ = без андервольту (типово)\n• SMU працює кроками по 6,25 мВ, тому значення змінюється на 6 або 7\n\nМенше напруги — нижчі температури й споживання, але надмір робить систему нестабільною. «Порадити зниження» запропонує значення, «Тест (бенчмарк)» його перевірить.",
    "CPU temperature limit": "Обмеження температури ЦП",
    "Thermal threshold above which the SMU throttles frequency.\n\nRecommended 85 °C. Above it, the system automatically lowers the clock to protect the hardware.":
        "Тепловий поріг, вище якого SMU знижує частоту.\n\nРекомендовано 85 °C. Вище цього значення система автоматично знижує частоту, щоб захистити обладнання.",
    "Max GPU frequency": "Максимальна частота ГП",
    "Maximum GPU clock (SMU governor).\n\n• 1500 = power saving\n• 2000 = balanced (safe on every board)\n• 2200 = max stable at 1000 mV on stock cooling\n\nThe GPU reaches this frequency only under load, then returns to 350 MHz at idle. Above 2200 MHz at 1000 mV the BC-250 can hard-freeze: it needs more voltage and depends on the silicon lottery.":
        "Максимальна частота ГП (регулятор SMU).\n\n• 1500 = енергозбереження\n• 2000 = збалансовано (безпечно на будь-якій платі)\n• 2200 = максимум стабільно за 1000 мВ зі штатним охолодженням\n\nГП досягає цієї частоти лише під навантаженням, а в спокої повертається до 350 МГц. Вище 2200 МГц за 1000 мВ BC-250 може повністю зависнути: потрібна більша напруга, і багато залежить від кремнієвої лотереї.",
    "Max GPU voltage": "Максимальна напруга ГП",
    "Voltage tied to the max GPU frequency (mV).\n\nHigher = more stable at high frequency but hotter. Range 700-1129 mV.":
        "Напруга, прив’язана до максимальної частоти ГП (мВ).\n\nБільше = стабільніше на високій частоті, але гарячіше. Діапазон 700-1129 мВ.",
    "GPU governor mode": "Режим регулятора ГП",
    "How the governor picks the GPU clock under load:\n\n• Balanced: raises the clock only as much as needed — cooler and quieter (default).\n• Performance: holds the GPU at max clock under any gaming load (best FPS in GPU-bound games, more heat and power). It still returns to 350 MHz at idle.\n\nBoth modes use the governor's safe SMU path.":
        "Як регулятор обирає частоту ГП під навантаженням:\n\n• Збалансований: піднімає частоту лише настільки, наскільки потрібно — прохолодніше і тихіше (типово).\n• Продуктивність: тримає ГП на максимальній частоті за будь-якого ігрового навантаження (найкращий FPS в іграх, обмежених ГП, більше тепла і споживання). У спокої так само повертається до 350 МГц.\n\nОбидва режими використовують безпечний шлях SMU регулятора.",
    "VRAM (graphics memory)": "VRAM (графічна пам’ять)",
    "Tuner log": "Журнал Tuner",
    "Refresh": "Оновити",
    "Copy": "Копіювати",
    "Close": "Закрити",
    "Copied to the clipboard.": "Скопійовано в буфер обміну.",
    "Amount of system RAM reserved for the GPU (UMA).\n\n⚠ Writes to the BIOS CMOS and requires a REBOOT to apply. Reversible by clearing the CMOS (jumper on the board).\n\nDon't exceed values that leave too little RAM for the system.":
        "Обсяг системної пам’яті, зарезервований для ГП (UMA).\n\n⚠ Записується у CMOS BIOS і потребує ПЕРЕЗАВАНТАЖЕННЯ. Скасовується очищенням CMOS (перемичка на платі).\n\nНе задавайте значень, після яких системі залишається замало пам’яті.",
    "Compute Units (CU)": "Обчислювальні блоки (CU)",
    "CUs are the GPU's compute cores. Here you enable/disable them LIVE, no reboot.\n\n• Each square = 1 CU: GREEN = active, RED = off.\n• CLICK a pair to toggle it manually (pairs are managed 2 CU at a time = 1 WGP).\n• Or use the PRESETS (24 / 32 / 40 CU).\n• The first 3 pairs per row (24 CU) are the driver minimum and stay always on.\n• Hit «Apply live» to make the choice effective.\n\nMore CU = more graphics power but also more heat and power draw.\n\nUse «Test CU» to verify the active CUs are stable and defect-free (silicon lottery).":
        "CU — це обчислювальні ядра ГП. Тут ви вмикаєте й вимикаєте їх НАЖИВО, без перезавантаження.\n\n• Кожен квадрат = 1 CU: ЗЕЛЕНИЙ = активний, ЧЕРВОНИЙ = вимкнений.\n• НАТИСНІТЬ на пару, щоб перемкнути її вручну (пари керуються по 2 CU = 1 WGP).\n• Або скористайтеся ЗАГОТОВКАМИ (24 / 32 / 40 CU).\n• Перші 3 пари в рядку (24 CU) — це мінімум драйвера, вони завжди увімкнені.\n• Натисніть «Застосувати наживо», щоб вибір набув чинності.\n\nБільше CU = більше графічної потужності, але й більше тепла та споживання.\n\nСкористайтеся «Тест CU», щоб перевірити, чи активні CU стабільні й без дефектів (кремнієва лотерея).",
    "daemon not running": "служба не запущена",
    "locked (always on)": "заблоковано (завжди увімкнено)",
    "on": "увімк.",
    "off": "вимк.",
    "◉  LIVE MONITOR": "◉  МОНІТОР НАЖИВО",
    "Temperature (°C)": "Температура (°C)",
    "Frequency (MHz)": "Частота (МГц)",
    "GPU voltage (mV)": "Напруга ГП (мВ)",
    "SkillFishOS — Monitor · %s": "SkillFishOS — Монітор · %s",
    "Help": "Довідка",
    "Presets": "Заготовки",
    "Apply": "Застосувати",
    "Extreme preset": "Екстремальна заготовка",
    "My silicon": "Мій кремній",
    "CPU %d MHz @ %d mV  ·  GPU %d MHz @ %d mV  ·  Active CUs: %d  ·  Detected freezes: %d":
        "ЦП %d МГц @ %d мВ  ·  ГП %d МГц @ %d мВ  ·  Активних CU: %d  ·  Виявлено зависань: %d",
    "⚠ Last freeze: %s — if it happens again, step down one notch with the wizards.":
        "⚠ Останнє зависання: %s — якщо повториться, знизьте на один щабель за допомогою майстрів.",
    "Share to the silicon DB": "Поділитися в базі кремнію",
    "Max frequency": "Максимальна частота",
    "Undervolt": "Зниження напруги",
    "Temperature limit": "Обмеження температури",
    "Find my max": "Знайти мій максимум",
    "Suggest UV": "Порадити зниження",
    "Test (benchmark)": "Тест (бенчмарк)",
    "CPU applied": "ЦП застосовано",
    "Save at boot": "Зберегти для завантаження",
    "CPU saved at boot": "ЦП збережено для завантаження",
    "CPU test: applying and benchmarking 60s (for a realistic temp)…":
        "Тест ЦП: застосування і бенчмарк 60 с (для реалістичної температури)…",
    "test failed": "тест не вдався",
    "Max voltage": "Максимальна напруга",
    "Balanced": "Збалансований",
    "Performance": "Продуктивність",
    "Governor: Balanced": "Регулятор: збалансований",
    "Governor: Performance — 2230 MHz under load":
        "Регулятор: продуктивність — 2230 МГц під навантаженням",
    "Governor mode": "Режим регулятора",
    "GPU applied": "ГП застосовано",
    "The wizard steps the GPU up (2000 → 2200 MHz at 1000 mV), validating each step with a benchmark and rolling back if unstable.\n\n~2 minutes per step under full GPU load.\nWhen done it applies the highest stable point for YOUR board.\n\nProceed?":
        "Майстер піднімає ГП щабель за щаблем (2000 → 2200 МГц за 1000 мВ), перевіряючи кожен щабель бенчмарком і відкочуючи його, якщо нестабільно.\n\n~2 хвилини на щабель за повного навантаження ГП.\nНаприкінці застосовується найвища стабільна точка саме ВАШОЇ плати.\n\nПродовжити?",
    "✗ Not even 2000 MHz is stable on this board: keeping the current profile.":
        "✗ Навіть 2000 МГц не є стабільними на цій платі: залишаємо поточний профіль.",
    "Highest stable point: %d MHz @ 1000 mV%s (already applied).":
        "Найвища стабільна точка: %d МГц @ 1000 мВ%s (уже застосовано).",
    "Share the result (anonymous) in the silicon-lottery database on GitHub?":
        "Поділитися результатом (анонімно) у базі кремнієвої лотереї на GitHub?",
    "Wizard result": "Результат майстра",
    "Find my max (CPU)": "Знайти мій максимум (ЦП)",
    "The wizard steps the CPU up (3600 → 4000 MHz with progressive undervolt), validating each step with a benchmark and stopping at the first unstable one.\n\n⚠ The last step can freeze some boards: the watchdog then reboots automatically and the last stable value is restored at boot.\n\nNote: the test validates UNDER-LOAD stability; idle stability over days is confirmed by regular use.\n\nProceed?":
        "Майстер піднімає ЦП щабель за щаблем (3600 → 4000 МГц із поступовим зниженням напруги), перевіряючи кожен щабель бенчмарком і зупиняючись на першому нестабільному.\n\n⚠ Останній щабель може завісити деякі плати: тоді сторожовий таймер автоматично перезавантажує систему, а при завантаженні відновлюється останнє стабільне значення.\n\nПримітка: тест перевіряє стабільність ПІД НАВАНТАЖЕННЯМ; стабільність у спокої протягом днів підтверджується звичайним користуванням.\n\nПродовжити?",
    "✗ Not even 3600 MHz is stable: keeping the current profile.":
        "✗ Навіть 3600 МГц не є стабільними: залишаємо поточний профіль.",
    "Tip: run the system for a few days; if a freeze appears, step down one notch.\n\nShare the result (anonymous) in the silicon-lottery database?":
        "Порада: покористуйтеся системою кілька днів; якщо з’явиться зависання, знизьте на один щабель.\n\nПоділитися результатом (анонімно) у базі кремнієвої лотереї?",
    "CPU wizard result": "Результат майстра ЦП",
    "GPU test: applying and running vkpeak…": "Тест ГП: застосування і запуск vkpeak…",
    "Compute Units (CU) — live": "Обчислювальні блоки (CU) — наживо",
    "40 CU (max)": "40 CU (макс.)",
    "Test CU": "Тест CU",
    "Active CUs: %d / 40": "Активних CU: %d / 40",
    "CUs applied: %s/40 (live)": "CU застосовано: %s/40 (наживо)",
    "CU error: %s": "Помилка CU: %s",
    "CU test": "Тест CU",
    "Tests the extra CUs (WGP3-4) one by one to catch defects (silicon lottery): enables each pair alone, stresses it with vkpeak and checks for GPU errors/hangs.\n\nTakes ~2-3 minutes and the GPU will be under load. Proceed?":
        "Перевіряє додаткові CU (WGP3-4) по одному, щоб виявити дефекти (кремнієва лотерея): вмикає кожну пару окремо, навантажує її через vkpeak і перевіряє помилки та зависання ГП.\n\nТриває ~2-3 хвилини, ГП буде під навантаженням. Продовжити?",
    "CU test running (~2-3 min)…": "Виконується тест CU (~2-3 хв)…",
    "CU test failed: %s": "Тест CU не вдався: %s",
    "40 CU under load (vkpeak): %s GFLOPS%s": "40 CU під навантаженням (vkpeak): %s GFLOPS%s",
    "Baseline 24 CU: %s GFLOPS": "База 24 CU: %s GFLOPS",
    "(expected ~11300 at 40 CU)": "(очікується ~11300 на 40 CU)",
    "Per extra-CU-pair stability:": "Стабільність кожної додаткової пари CU:",
    "⚠ GPU problems under load detected: possible defective CU (silicon lottery). Consider keeping fewer CUs.":
        "⚠ Виявлено проблеми ГП під навантаженням: можливий дефектний CU (кремнієва лотерея). Варто залишити менше CU.",
    "✓ No defects: all CUs sustain the load with no GPU errors.":
        "✓ Дефектів немає: усі CU витримують навантаження без помилок ГП.",
    "CU test result": "Результат тесту CU",
    "Manual control (off = automatic curve)": "Ручне керування (вимкнено = автоматична крива)",
    "Speed": "Швидкість",
    "Test": "Тест",
    "New allocation": "Новий обсяг",
    "Set (reboot)": "Встановити (перезавантаження)",
    "Writes to the BIOS CMOS. A reboot is needed to apply.":
        "Записується у CMOS BIOS. Для застосування потрібне перезавантаження.",
    "Authentication cancelled or helper not started.\nRestart the program and authorize the operation.":
        "Автентифікацію скасовано або службу-помічника не запущено.\nПерезапустіть програму й дозвольте виконання операції.",
    # pannello core CPU: mancava nella versione da cui e' stata
    # fatta la traduzione, queste stringhe sono mie
    "CPU cores": "Ядра ЦП",
    "Turn CPU cores on and off LIVE, no reboot — just like the GPU's CUs.\n\n• Each box = 1 physical core with its 2 threads: GREEN = on, RED = off.\n• CLICK a core to toggle it, then «Apply».\n• Core 0 is FIXED GREEN: it holds the boot CPU and the kernel cannot offline it.\n• «SMT» toggles multi-threading: 2 threads per core, or 1.\n\nWhy: switching cores off frees thermal and power budget so the remaining cores can clock higher — handy for games that use few threads. Fewer cores = less heat, but less multi-threaded power.\n\nThe choice is NOT permanent: everything comes back on at reboot.":
        "Вмикайте й вимикайте ядра ЦП НАЖИВО, без перезавантаження — так само, як CU у ГП.\n\n• Кожен квадрат = 1 фізичне ядро з його 2 потоками: ЗЕЛЕНИЙ = увімкнено, ЧЕРВОНИЙ = вимкнено.\n• НАТИСНІТЬ на ядро, щоб перемкнути його, потім «Застосувати».\n• Ядро 0 завжди ЗЕЛЕНЕ: на ньому працює завантажувальний процесор, і ядро системи не може його вимкнути.\n• «SMT» перемикає багатопотоковість: 2 потоки на ядро або 1.\n\nНавіщо: вимкнення ядер вивільняє тепловий та енергетичний бюджет, тож решта ядер можуть працювати на вищій частоті — корисно в іграх, що використовують мало потоків. Менше ядер = менше тепла, але менше потужності в багатопотокових задачах.\n\nВибір НЕ є постійним: після перезавантаження всі повертаються.",
    "CPU cores — live": "Ядра ЦП — наживо",
    "Multi-threading: 2 threads per core. Off = 1 thread per core.":
        "Багатопотоковість: 2 потоки на ядро. Вимкнено = 1 потік на ядро.",
    "boot core (always on)": "завантажувальне ядро (завжди увімкнене)",
    "All": "Усі",
    "Active cores: %d / %d  ·  %d threads": "Активних ядер: %d / %d  ·  %d потоків",
    "SMT %s — %d threads": "SMT %s — %d потоків",
    "SMT: %s": "SMT: %s",
    "Cores applied — %d active threads": "Ядра застосовано — %d активних потоків",
    "Core error: %s": "Помилка ядра: %s",
}

def L(it, en):
    """Restituisce la stringa IT, PL, UK o EN secondo il locale rilevato."""
    if LANG == "it": return it
    if LANG == "pl": return PL.get(en) or _TR(en)
    if LANG == "uk": return UK.get(en) or _TR(en)
    return _TR(en)


HELP = {
 "cpu_freq": (L("Frequenza massima CPU", "Max CPU frequency"),
   L("Limite di boost della CPU in MHz.\n\n• 3500 = base sicura\n• 3700 = stabile verificato su questo chip\n• 3800-3900 = oltre i limiti, possibile throttling termico\n\nIl voltaggio (Vid) resta sempre sotto 1.325 V per sicurezza.",
     "CPU boost limit in MHz.\n\n• 3500 = safe base\n• 3700 = verified stable on this chip\n• 3800-3900 = beyond limits, possible thermal throttling\n\nVoltage (Vid) always stays below 1.325 V for safety.")),
 "cpu_scale": (L("Undervolt (mV)", "Undervolt (mV)"),
   L("Riduce il voltaggio della CPU sotto la curva di fabbrica.\n\n• 0 mV = nessun undervolt (predefinito)\n• l'SMU si muove a scalini da 6,25 mV, quindi il valore salta di 6 o 7\n\nMeno voltaggio vuol dire temperature più basse e meno consumo, ma se si esagera il sistema diventa instabile. «Suggerisci UV» propone un valore, «Test (benchmark)» lo verifica.",
     "Lowers the CPU voltage below the factory curve.\n\n• 0 mV = no undervolt (default)\n• the SMU moves in steps of 6.25 mV, so the value jumps by 6 or 7\n\nLess voltage means lower temperatures and less power, but going too far makes the system unstable. «Suggest UV» proposes a value, «Test (benchmark)» verifies it.")),
 "cpu_temp": (L("Limite temperatura CPU", "CPU temperature limit"),
   L("Soglia termica oltre la quale l'SMU limita la frequenza.\n\nConsigliato 85 °C. Oltre, il sistema riduce automaticamente il clock per proteggere l'hardware.",
     "Thermal threshold above which the SMU throttles frequency.\n\nRecommended 85 °C. Above it, the system automatically lowers the clock to protect the hardware.")),
 "gpu_freq": (L("Frequenza massima GPU", "Max GPU frequency"),
   L("Clock massimo della GPU (governor SMU).\n\n• 1500 = risparmio\n• 2000 = bilanciato (sicuro su tutte le schede)\n• 2200 = massimo stabile a 1000 mV su raffreddamento stock\n\nLa GPU sale a questa frequenza solo sotto carico, poi torna a 350 MHz a riposo. Oltre i 2200 MHz a 1000 mV il BC-250 può bloccarsi: serve più voltaggio e la lotteria del silicio.",
     "Maximum GPU clock (SMU governor).\n\n• 1500 = power saving\n• 2000 = balanced (safe on every board)\n• 2200 = max stable at 1000 mV on stock cooling\n\nThe GPU reaches this frequency only under load, then returns to 350 MHz at idle. Above 2200 MHz at 1000 mV the BC-250 can hard-freeze: it needs more voltage and depends on the silicon lottery.")),
 "gpu_volt": (L("Voltaggio massimo GPU", "Max GPU voltage"),
   L("Voltaggio associato alla frequenza massima GPU (mV).\n\nPiù alto = più stabile ad alte frequenze ma più caldo. Range 700-1129 mV.",
     "Voltage tied to the max GPU frequency (mV).\n\nHigher = more stable at high frequency but hotter. Range 700-1129 mV.")),
 "gov_mode": (L("Modalità governor GPU", "GPU governor mode"),
   L("Come il governor sceglie il clock GPU sotto carico:\n\n• Bilanciato: alza il clock solo quanto serve — più fresco e silenzioso (default).\n• Performance: tiene la GPU al clock massimo sotto qualsiasi carico di gioco (massimi FPS nei giochi GPU-bound, più calore e consumo). A riposo torna comunque a 350 MHz.\n\nEntrambe le modalità usano la via SMU sicura del governor.",
     "How the governor picks the GPU clock under load:\n\n• Balanced: raises the clock only as much as needed — cooler and quieter (default).\n• Performance: holds the GPU at max clock under any gaming load (best FPS in GPU-bound games, more heat and power). It still returns to 350 MHz at idle.\n\nBoth modes use the governor's safe SMU path.")),
 "vram": (L("VRAM (memoria grafica)", "VRAM (graphics memory)"),
   L("Quantità di RAM di sistema riservata alla GPU (UMA).\n\n⚠ Scrive nel CMOS del BIOS e richiede un RIAVVIO per applicare. Reversibile azzerando il CMOS (jumper sulla scheda).\n\nNon superare valori che lascino troppo poca RAM al sistema.",
     "Amount of system RAM reserved for the GPU (UMA).\n\n⚠ Writes to the BIOS CMOS and requires a REBOOT to apply. Reversible by clearing the CMOS (jumper on the board).\n\nDon't exceed values that leave too little RAM for the system.")),
 "cpucores": (L("Core CPU", "CPU cores"),
   L("Accendi e spegni i core della CPU A CALDO, senza riavvio — come per le CU della GPU.\n\n• Ogni riquadro = 1 core fisico con i suoi 2 thread: VERDE = acceso, ROSSO = spento.\n• CLIC su un core per accenderlo/spegnerlo, poi «Applica».\n• Il core 0 è VERDE FISSO: ospita la CPU di avvio e il kernel non può spegnerlo.\n• «SMT» accende/spegne il multi-threading: da 2 thread per core a 1.\n\nA cosa serve: spegnere core libera budget termico e di consumo per far salire la frequenza dei core rimasti — utile nei giochi che usano pochi thread. Meno core = meno calore, ma meno potenza nei carichi multi-thread.\n\nLa scelta NON è permanente: al riavvio tornano tutti accesi.",
     "Turn CPU cores on and off LIVE, no reboot — just like the GPU's CUs.\n\n• Each box = 1 physical core with its 2 threads: GREEN = on, RED = off.\n• CLICK a core to toggle it, then «Apply».\n• Core 0 is FIXED GREEN: it holds the boot CPU and the kernel cannot offline it.\n• «SMT» toggles multi-threading: 2 threads per core, or 1.\n\nWhy: switching cores off frees thermal and power budget so the remaining cores can clock higher — handy for games that use few threads. Fewer cores = less heat, but less multi-threaded power.\n\nThe choice is NOT permanent: everything comes back on at reboot.")),
 "cu": (L("Compute Unit (CU)", "Compute Units (CU)"),
   L("Le CU sono i core di calcolo della GPU. Qui le attivi/disattivi A CALDO, senza riavvio.\n\n• Ogni quadratino = 1 CU: VERDE = attiva, ROSSO = spenta.\n• CLIC su una coppia per accenderla/spegnerla manualmente (le coppie si gestiscono a 2 CU per volta = 1 WGP).\n• Oppure usa i PRESET (24 / 32 / 40 CU).\n• Le prime 3 coppie per fila (24 CU) sono il minimo del driver e restano sempre attive.\n• Premi «Applica a caldo» per rendere effettiva la scelta.\n\nPiù CU = più potenza grafica ma anche più calore e consumo.\n\nUsa «Test CU» per verificare che le CU attive siano stabili e senza difetti (lotteria del silicio).",
     "CUs are the GPU's compute cores. Here you enable/disable them LIVE, no reboot.\n\n• Each square = 1 CU: GREEN = active, RED = off.\n• CLICK a pair to toggle it manually (pairs are managed 2 CU at a time = 1 WGP).\n• Or use the PRESETS (24 / 32 / 40 CU).\n• The first 3 pairs per row (24 CU) are the driver minimum and stay always on.\n• Hit «Apply live» to make the choice effective.\n\nMore CU = more graphics power but also more heat and power draw.\n\nUse «Test CU» to verify the active CUs are stable and defect-free (silicon lottery).")),
}



# ColonneFlusso: mette le sezioni in una, due o tre colonne secondo lo spazio.
# 
# IL PROBLEMA MISURATO
# La finestra del Tuner e' 1000x1012 su un'area utile di 1920x1040: alta quanto
# tutto lo schermo, e continua comunque a scorrere. Nel frattempo meta' della
# larghezza non la usa nessuno. Le otto sezioni stanno in una colonna sola, e una
# colonna sola non ha modo di sfruttare uno schermo largo.
# 
# PERCHE' NON BASTA ALLARGARE E BASTA
# Perche' il Tuner deve girare anche su schermi piu' piccoli — un portatile a
# 1366x768, una TV a 1280x720. Una disposizione a due colonne fissa risolverebbe
# il 1920 e romperebbe il 1366. Serve che il numero di colonne lo decida lo
# spazio, e che cambi da solo quando la finestra cambia dimensione.
# 
# COME FUNZIONA
# Le sezioni vengono distribuite nella colonna PIU' CORTA al momento, nell'ordine
# in cui sono state aggiunte. Non e' un riempimento perfetto (sarebbe un problema
# di bin packing), ma tiene le sezioni importanti in alto a sinistra e pareggia
# le altezze abbastanza da non lasciare una colonna vuota a meta'.
# 
# ⚠️ heightForWidth deve rifare lo STESSO conto di setGeometry. Se i due si
# scostano, l'area scorrevole calcola un'altezza diversa da quella che poi
# disegna: compare una barra di scorrimento per pochi pixel, o ne manca una che
# servirebbe. E' la classe di errore piu' facile da introdurre qui.

class ColonneFlusso(QLayout):

    def __init__(self, parent=None, margine=0, spazio=10, larghezza_min=430, colonne_max=3):
        super().__init__(parent)
        self._voci = []
        self._spazio = spazio
        self.larghezza_min = larghezza_min
        self.colonne_max = colonne_max
        self.setContentsMargins(margine, margine, margine, margine)

    # --- gli obblighi di QLayout ------------------------------------------
    def addItem(self, item):
        self._voci.append(item)

    def count(self):
        return len(self._voci)

    def itemAt(self, i):
        return self._voci[i] if 0 <= i < len(self._voci) else None

    def takeAt(self, i):
        return self._voci.pop(i) if 0 <= i < len(self._voci) else None

    def expandingDirections(self):
        return Qt.Orientation(0)

    def hasHeightForWidth(self):
        return True

    def heightForWidth(self, larghezza):
        return self._disponi(QRect(0, 0, larghezza, 0), prova=True)

    def setGeometry(self, rect):
        super().setGeometry(rect)
        self._disponi(rect, prova=False)

    def sizeHint(self):
        return self.minimumSize()

    def minimumSize(self):
        # La minima e' UNA colonna larga quanto la sezione piu' larga: sotto
        # quella larghezza i cursori e i bottoni si schiacciano.
        w = 0
        for v in self._voci:
            w = max(w, v.minimumSize().width())
        s, d, _, _ = self.getContentsMargins()
        return QSize(max(w, self.larghezza_min) + s + d, 0)

    # --- il conto vero -----------------------------------------------------
    def quante_colonne(self, larghezza):
        s, d, _, _ = self.getContentsMargins()
        utile = larghezza - s - d
        n = (utile + self._spazio) // (self.larghezza_min + self._spazio)
        return max(1, min(int(n), self.colonne_max, len(self._voci) or 1))

    def _disponi(self, rect, prova):
        if not self._voci:
            return 0
        s, sup, d, inf = self.getContentsMargins()
        n = self.quante_colonne(rect.width())
        utile = rect.width() - s - d
        larg = (utile - (n - 1) * self._spazio) // n

        alte = [0] * n            # altezza accumulata di ogni colonna
        for v in self._voci:
            # la colonna piu' corta al momento; a parita', la piu' a sinistra
            c = alte.index(min(alte))
            h = v.sizeHint().height()
            if v.hasHeightForWidth():
                h = v.heightForWidth(larg)
            if not prova:
                x = rect.x() + s + c * (larg + self._spazio)
                y = rect.y() + sup + alte[c]
                v.setGeometry(QRect(x, y, larg, h))
            alte[c] += h + self._spazio

        # l'ultimo spazio non conta: sta sotto all'ultima sezione
        return max(alte) - self._spazio + sup + inf


# ---------- il giornale ----------
# ⚠️ Serve a chi ci scrive «non funziona». Prima non c'era niente da guardare:
# il Tuner parlava con l'aiutante e quello scambio spariva, cosi' l'unica cosa
# che l'utente poteva riferirci era il testo del messaggio d'errore.
GIORNALE = collections.deque(maxlen=400)


def annota(riga):
    GIORNALE.append("%s  %s" % (time.strftime("%H:%M:%S"), riga))


def giornale_testo():
    u"""Il giornale con in cima le quattro cose che chiederemmo comunque."""
    def leggi(cmd):
        try:
            return subprocess.run(cmd, shell=True, capture_output=True,
                                  text=True, timeout=5).stdout.strip() or "-"
        except Exception as e:
            return "(%s)" % e
    testa = [
        "SkillFishOS Tuner",
        "kernel:  " + leggi("uname -r"),
        "pacchetto: " + leggi("dpkg-query -W -f='${Version}' skillfish-tuner 2>/dev/null"),
        # ⚠️ NON `skillfish-cu get`: per leggere i registri vuole root, e il
        # Tuner gira da utente normale — usciva active_cu 0, cioe' un numero
        # falso in cima al testo che l'utente ci manda. Lo stato vero
        # skillfish-cu lo pubblica qui, leggibile a tutti.
        "CU:      " + leggi("cat /run/skillfish/cu_active.json 2>/dev/null "
                            "|| echo 'stato non pubblicato'"),
        "cmdline: " + leggi("cat /proc/cmdline"),
        "-" * 60,
    ]
    return "\n".join(testa + list(GIORNALE))


class FinestraGiornale(QDialog):
    u"""Il giornale in una finestra, con un pulsante che lo copia."""

    def __init__(self, padre=None):
        super().__init__(padre)
        self.setWindowTitle(L("Log del Tuner", "Tuner log"))
        self.resize(760, 460)
        v = QVBoxLayout(self)
        self.testo = QPlainTextEdit()
        self.testo.setReadOnly(True)
        self.testo.setStyleSheet("font-family:monospace;font-size:11px;")
        v.addWidget(self.testo)
        riga = QHBoxLayout()
        riga.addStretch(1)
        b_agg = QPushButton(L("Aggiorna", "Refresh"))
        b_agg.clicked.connect(self.ricarica)
        b_cop = QPushButton(L("Copia", "Copy"))
        b_cop.clicked.connect(self.copia)
        b_chi = QPushButton(L("Chiudi", "Close"))
        b_chi.clicked.connect(self.close)
        for b in (b_agg, b_cop, b_chi):
            riga.addWidget(b)
        v.addLayout(riga)
        self.avviso = QLabel("")
        self.avviso.setStyleSheet("color:#8fbf6a;font-size:11px;")
        v.addWidget(self.avviso)
        self.ricarica()

    def ricarica(self):
        self.testo.setPlainText(giornale_testo())
        self.testo.verticalScrollBar().setValue(
            self.testo.verticalScrollBar().maximum())

    def copia(self):
        QApplication.clipboard().setText(self.testo.toPlainText())
        self.avviso.setText(L("Copiato negli appunti.", "Copied to the clipboard."))


class Daemon:
    """Persistent root helper via pkexec: a single authentication at startup."""
    def __init__(self):
        self.proc = None
    def start(self):
        try:
            self.proc = subprocess.Popen(["pkexec", HELPER], stdin=subprocess.PIPE,
                                         stdout=subprocess.PIPE, text=True, bufsize=1)
        except Exception:
            return False
        line = self.proc.stdout.readline()      # "ready" line (after auth)
        try:
            return json.loads(line).get("ready", False)
        except Exception:
            return False
    def cmd(self, **kw):
        # ⚠️ Ogni scambio finisce nel giornale: e' l'unica traccia che resta di
        # cosa ha chiesto la finestra e cosa ha risposto il sistema, e senza
        # quella a un utente che segnala un guasto non si puo' chiedere niente
        # di utile.
        annota("-> %s" % json.dumps(kw)[:400])
        if not self.proc or self.proc.poll() is not None:
            r = {"ok": False, "err": L("daemon non attivo", "daemon not running")}
            annota("<- %s" % json.dumps(r))
            return r
        try:
            self.proc.stdin.write(json.dumps(kw) + "\n"); self.proc.stdin.flush()
            r = json.loads(self.proc.stdout.readline())
        except Exception as e:
            r = {"ok": False, "err": str(e)}
        annota("<- %s" % json.dumps(r, ensure_ascii=False)[:400])
        return r
    def stop(self):
        try:
            if self.proc and self.proc.poll() is None:
                self.proc.stdin.write('{"cmd":"quit"}\n'); self.proc.stdin.flush()
        except Exception:
            pass  # best-effort: daemon may already be gone


class CmdThread(QThread):
    """Runs a blocking function (benchmark) off the UI thread."""
    done = pyqtSignal(object)
    def __init__(self, fn):
        super().__init__(); self.fn = fn
    def run(self):
        self.done.emit(self.fn())


# Un scalino di undervolt della CPU quanto vale in millivolt.
# Misurato sulla scheda il 05/06/2026 leggendo la tensione vera dall'SMU
# (q3_0x36) a 3700 MHz sotto carico: scale 0 = 1206 mV, scale -32 = 1012 mV,
# cioe' 194 mV in 32 scalini. E' il passo VID standard di AMD.
MV_PER_SCALINO = 6.25


def mv_undervolt(scalini):
    """Da scalini dell'SMU a millivolt, come li legge una persona."""
    return "0 mV" if not scalini else "%d mV" % round(scalini * MV_PER_SCALINO)


class Stepper(QWidget):
    """[-] [slider] [+] [value] — fine ±1 increment on the buttons.

    «mostra» serve quando il numero che vuole l'hardware non e' il numero che
    ha senso per chi guarda: l'undervolt della CPU si manda all'SMU a scalini,
    ma si legge in millivolt.
    """
    def __init__(self, lo, hi, val, unit, mostra=None):
        super().__init__()
        self.lo, self.hi, self.unit, self.mostra = lo, hi, unit, mostra
        h = QHBoxLayout(self); h.setContentsMargins(0, 0, 0, 0); h.setSpacing(6)
        self.minus = QToolButton(); self.minus.setText("−")
        self.s = QSlider(Qt.Orientation.Horizontal); self.s.setRange(lo, hi); self.s.setValue(int(val))
        self.s.setMinimumWidth(170)
        self.plus = QToolButton(); self.plus.setText("+")
        self.lbl = QLabel(); self.lbl.setMinimumWidth(96)
        self.lbl.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
        self.lbl.setStyleSheet("color:#d8a849;font-weight:bold;")
        h.addWidget(self.minus); h.addWidget(self.s); h.addWidget(self.plus); h.addWidget(self.lbl)
        self.minus.clicked.connect(lambda: self.s.setValue(max(lo, self.s.value() - 1)))
        self.plus.clicked.connect(lambda: self.s.setValue(min(hi, self.s.value() + 1)))
        self.s.valueChanged.connect(self._upd); self._upd(int(val))
    def _upd(self, v):
        self.lbl.setText(self.mostra(v) if self.mostra else f"{v} {self.unit}".strip())
    def value(self):
        return self.s.value()


class CuWgp(QFrame):
    """One WGP rendered as two CU squares. Green=on, red=off, locked WGPs fixed green."""
    def __init__(self, on, locked, on_toggle):
        super().__init__()
        self.on = on; self.locked = locked; self.on_toggle = on_toggle
        lay = QHBoxLayout(self); lay.setContentsMargins(1, 1, 1, 1); lay.setSpacing(2)
        self.sq = [QFrame(), QFrame()]
        for s in self.sq:
            s.setFixedSize(16, 16); lay.addWidget(s)
        if not self.locked:
            self.setCursor(Qt.CursorShape.PointingHandCursor)
        self._paint()
    def _paint(self):
        if self.locked:  col, bd = "#3f7a39", "#2c571f"
        elif self.on:    col, bd = "#5fd24f", "#36862c"
        else:            col, bd = "#7c2424", "#4d1414"
        for s in self.sq:
            s.setStyleSheet("background:%s;border:1px solid %s;border-radius:3px;" % (col, bd))
        self.setToolTip(L("bloccata (sempre attiva)", "locked (always on)") if self.locked
                        else (L("attiva", "on") if self.on else L("spenta", "off")))
    def mousePressEvent(self, e):
        if self.locked: return
        self.on = not self.on; self._paint(); self.on_toggle(self.on)


class CpuCore(QFrame):
    """One physical CPU core drawn as its two SMT threads, styled like the CU grid.

    Core 0 is locked: it holds cpu0, the boot CPU, which this kernel cannot offline.
    """
    def __init__(self, core, on, locked, on_toggle):
        super().__init__()
        self.core = core; self.on = on; self.locked = locked; self.on_toggle = on_toggle
        lay = QVBoxLayout(self); lay.setContentsMargins(1, 1, 1, 1); lay.setSpacing(2)
        row = QHBoxLayout(); row.setSpacing(2)
        self.sq = [QFrame(), QFrame()]
        for s in self.sq:
            s.setFixedSize(16, 16); row.addWidget(s)
        rw = QWidget(); rw.setLayout(row); lay.addWidget(rw)
        cap = QLabel("C%d" % core); cap.setAlignment(Qt.AlignmentFlag.AlignHCenter)
        cap.setStyleSheet("color:#8f7c52;font-size:9px;"); lay.addWidget(cap)
        if not self.locked:
            self.setCursor(Qt.CursorShape.PointingHandCursor)
        self._paint()
    def _paint(self):
        if self.locked:  col, bd = "#3f7a39", "#2c571f"
        elif self.on:    col, bd = "#5fd24f", "#36862c"
        else:            col, bd = "#7c2424", "#4d1414"
        for s in self.sq:
            s.setStyleSheet("background:%s;border:1px solid %s;border-radius:3px;" % (col, bd))
        self.setToolTip(L("core di avvio (sempre attivo)", "boot core (always on)") if self.locked
                        else (L("attivo", "on") if self.on else L("spento", "off")))
    def mousePressEvent(self, e):
        if self.locked: return
        self.on = not self.on; self._paint(); self.on_toggle(self.core, self.on)


class Chart(QWidget):
    """A live, custom-painted line chart panel (brass / steampunk look)."""
    def __init__(self, title, unit, series):
        super().__init__()
        self.title = title; self.unit = unit; self.series = series
        self.data = [collections.deque(maxlen=180) for _ in series]
        self.setMinimumSize(400, 190)

    def push(self, vals):
        for d, v in zip(self.data, vals, strict=False):
            d.append(float(v) if v is not None else (d[-1] if d else 0.0))
        self.update()

    def paintEvent(self, _e):
        p = QPainter(self); p.setRenderHint(QPainter.RenderHint.Antialiasing)
        w, h = self.width(), self.height()
        p.setPen(QPen(QColor(216, 168, 73, 80), 1)); p.setBrush(QColor(18, 15, 9, 240))
        p.drawRoundedRect(1, 1, w - 2, h - 2, 14, 14)
        gx0, gy0, gx1, gy1 = 14, 34, w - 14, h - 16
        gw, gh = gx1 - gx0, gy1 - gy0
        f = QFont("Inter"); f.setBold(True); f.setPointSize(10); p.setFont(f)
        p.setPen(QColor(216, 168, 73)); p.drawText(14, 22, self.title)
        allv = [v for d in self.data for v in d]
        if not allv: p.end(); return
        lo, hi = min(allv), max(allv)
        if hi - lo < 1e-6: hi = lo + 1.0
        m = (hi - lo) * 0.14; lo -= m; hi += m
        p.setPen(QPen(QColor(216, 168, 73, 26), 1))
        for i in range(4):
            yy = int(gy0 + gh * i / 3); p.drawLine(gx0, yy, gx1, yy)
        fv = QFont("DejaVu Sans Mono"); fv.setPointSize(9); fv.setBold(True)
        lx = gx1
        for si, (label, col) in enumerate(self.series):
            d = self.data[si]; n = len(d); color = QColor(col)
            if n >= 2:
                line = QPainterPath(); fill = QPainterPath()
                for i, v in enumerate(d):
                    x = gx0 + gw * (i / (n - 1)); y = gy1 - gh * ((v - lo) / (hi - lo))
                    if i == 0: line.moveTo(x, y); fill.moveTo(x, gy1); fill.lineTo(x, y)
                    else: line.lineTo(x, y); fill.lineTo(x, y)
                fill.lineTo(gx1, gy1); fill.closeSubpath()
                grad = QLinearGradient(0, gy0, 0, gy1)
                c0 = QColor(color); c0.setAlpha(75); c1 = QColor(color); c1.setAlpha(0)
                grad.setColorAt(0, c0); grad.setColorAt(1, c1)
                p.setPen(Qt.PenStyle.NoPen); p.setBrush(QBrush(grad)); p.drawPath(fill)
                glow = QColor(color); glow.setAlpha(55)
                p.setBrush(Qt.BrushStyle.NoBrush); p.setPen(QPen(glow, 5)); p.drawPath(line)
                p.setPen(QPen(color, 2)); p.drawPath(line)
            cur = d[-1] if n else 0.0
            txt = "%s %s" % (label, ("%d" % round(cur)) if abs(cur) >= 10 else ("%.2f" % cur))
            p.setFont(fv); p.setPen(color)
            tw = p.fontMetrics().horizontalAdvance(txt)
            lx -= tw + 14; p.drawText(int(lx), 22, txt)
        p.end()


class MonitorWindow(QWidget):
    """Live sensor dashboard that pops up during a Tuner test. Closable by the user."""
    def __init__(self):
        super().__init__()
        self.setWindowTitle("SkillFishOS — Monitor")
        self.setWindowFlag(Qt.WindowType.Window, True)
        self.setWindowIcon(QIcon(ICON))
        self.resize(900, 560)
        self.setStyleSheet("QWidget{background:#0d0b06;}")
        grid = QGridLayout(self); grid.setContentsMargins(14, 12, 14, 12); grid.setSpacing(12)
        hdr = QLabel(L("◉  MONITOR LIVE", "◉  LIVE MONITOR"))
        hdr.setStyleSheet("color:#d8a849;font-weight:bold;font-size:13px;letter-spacing:3px;")
        grid.addWidget(hdr, 0, 0, 1, 2)
        self.ch_temp = Chart(L("Temperatura (°C)", "Temperature (°C)"), "°C", [("CPU", "#e8c878"), ("GPU", "#e07b39")])
        self.ch_freq = Chart(L("Frequenza (MHz)", "Frequency (MHz)"), "MHz", [("CPU", "#5fd24f"), ("GPU", "#49b6e0")])
        self.ch_volt = Chart(L("Voltaggio GPU (mV)", "GPU voltage (mV)"), "mV", [("GPU", "#c98be0")])
        grid.addWidget(self.ch_temp, 1, 0); grid.addWidget(self.ch_freq, 1, 1)
        # ⚠️ Il voltaggio prende tutta la riga: qui accanto c'era il grafico dei
        # giri della ventola, che ora sta in SkillFishOS Fan Control insieme al
        # suo comando. Lasciare mezza riga vuota sarebbe peggio.
        grid.addWidget(self.ch_volt, 2, 0, 1, 2)
        self.timer = QTimer(self); self.timer.setInterval(700); self.timer.timeout.connect(self._tick)

    def start(self):
        if not self.timer.isActive(): self.timer.start()

    def closeEvent(self, e):
        self.timer.stop(); super().closeEvent(e)

    def _rd(self, key):
        try:
            out = subprocess.run(["skillfish-hud-val", key], capture_output=True, text=True, timeout=0.6).stdout
            m = re.search(r'-?\d+(?:\.\d+)?', out)
            return float(m.group()) if m else None
        except Exception:
            return None

    def _tick(self):
        self.ch_temp.push([self._rd("cpu_temp"), self._rd("gpu_temp")])
        self.ch_freq.push([self._rd("cpu_mhz"), self._rd("gpu_freq")])
        self.ch_volt.push([self._rd("gpu_mv")])


class TunerWindow(QMainWindow):
    def __init__(self, daemon):
        super().__init__()
        self.d = daemon
        self.setWindowTitle("SkillFishOS Tuner")
        self.setWindowIcon(QIcon(ICON))
        self._geomfile = os.path.expanduser("~/.config/skillfishos/tuner-geom.json")
        self._geom_salvata = None
        try:
            with open(self._geomfile) as _gf:
                gj = json.load(_gf)
            self._geom_salvata = (int(gj["w"]), int(gj["h"]))
        except Exception:
            pass
        self.cfg = (self.d.cmd(cmd="get").get("data")) or {}
        self._threads = []

        scroll = QScrollArea(); scroll.setWidgetResizable(True)
        # ⚠️ Niente barra ORIZZONTALE: la disposizione a colonne si stringe da
        # sola fino a una colonna, quindi una barra orizzontale vorrebbe dire
        # solo che qualcosa dentro non si e' voluto stringere. Meglio vederlo.
        scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
        body = QWidget()
        # Le sezioni si dispongono in una, due o tre colonne secondo la
        # larghezza: e' cosi' che la finestra entra in uno schermo largo senza
        # rompersi su uno stretto.
        #
        # ⚠️ Provate anche le linguette (quattro schede da due sezioni), poi
        # tolte: la pagina unica si e' rivelata piu' bella da guardare, e su
        # uno schermo largo mostra tutto senza bisogno di cambiare pagina.
        self.box = ColonneFlusso(body, margine=14, spazio=12, larghezza_min=430)
        scroll.setWidget(body); self.setCentralWidget(scroll)
        self.statusBar()

        self.box.addWidget(self._presets())
        self.box.addWidget(self._silicon())
        self.box.addWidget(self._cpu())
        self.box.addWidget(self._gpu())
        self.box.addWidget(self._vram())
        self.box.addWidget(self._cu())
        self.box.addWidget(self._sec_cpu_cores())
        self._adatta_allo_schermo()

    def _adatta_allo_schermo(self):
        u"""Sceglie la dimensione guardando lo schermo, non un numero scritto a mano.

        ⚠️ Il vecchio 982x1000 era il problema, non la soluzione: con 982 di
        larghezza le sezioni stanno in una colonna sola su QUALUNQUE monitor, e
        una colonna sola di otto sezioni non entra da nessuna parte. Qui la
        larghezza si prende dallo schermo, e l'altezza si chiede alla
        disposizione — che a quella larghezza sa quanto le serve davvero.
        """
        scr = self.screen() or QApplication.primaryScreen()
        disp = scr.availableGeometry() if scr else None
        if disp is None:
            self.resize(1200, 800)
            return
        if self._geom_salvata:
            # la dimensione scelta dall'utente vale, ma non oltre lo schermo:
            # una finestra salvata su un monitor grande e riaperta su uno
            # piccolo resterebbe con i bottoni fuori dal bordo.
            w, h = self._geom_salvata
            self.resize(min(w, disp.width()), min(h, disp.height()))
            return
        # Con OTTO sezioni su una pagina sola prendersi la larghezza dello
        # schermo e' giusto: piu' larghezza vuol dire piu' colonne e quindi
        # meno altezza. (Con le linguette non lo era, ed e' per questo che la
        # regola era stata cambiata: tolte quelle, torna questa.)
        larg = min(int(disp.width() * 0.94), 1560)
        alta = self.box.heightForWidth(larg - 40) + 90   # barra di stato e cornice
        self.resize(larg, min(alta, int(disp.height() * 0.94)))

    # ---------- UI helpers ----------
    def toast(self, msg, secs=4):
        annota("messaggio: %s" % msg)
        self.statusBar().showMessage(msg, secs * 1000)

    def _apri_giornale(self):
        if getattr(self, "_giornale", None) is None:
            self._giornale = FinestraGiornale(self)
        self._giornale.ricarica()
        self._giornale.show(); self._giornale.raise_(); self._giornale.activateWindow()

    def _open_monitor(self, label):
        if getattr(self, "_mon", None) is None:
            self._mon = MonitorWindow()
        self._mon.setWindowTitle(L("SkillFishOS — Monitor · %s", "SkillFishOS — Monitor · %s") % label)
        self._mon.start(); self._mon.show(); self._mon.raise_(); self._mon.activateWindow()

    def closeEvent(self, e):
        # merged: persist geometry, close the monitor window, stop the daemon, accept.
        try:
            os.makedirs(os.path.dirname(self._geomfile), exist_ok=True)
            with open(self._geomfile, "w") as _gf:
                json.dump({"w": self.width(), "h": self.height()}, _gf)
        except Exception:
            pass  # best-effort: persisting window geometry is non-critical
        if getattr(self, "_mon", None) is not None:
            self._mon.close()
        try:
            self.d.stop()
        except Exception:
            pass  # best-effort: daemon may already be stopped
        e.accept()
        super().closeEvent(e)

    def _help_btn(self, key):
        # Il pallino condiviso (/usr/share/skillfish/aiuto.py): stesso aspetto
        # in tutte le applicazioni, e decide lui se il testo sta in una
        # bollicina o vuole un riquadro. Gli aiuti del Tuner sono lunghi, quindi
        # in pratica escono quasi tutti come riquadro, com'era prima.
        titolo, corpo = HELP.get(key, (L("Aiuto", "Help"), ""))
        return Aiuto(corpo, titolo, self)

    def _row(self, title, widget, helpkey=None):
        w = QWidget(); h = QHBoxLayout(w); h.setContentsMargins(0, 0, 0, 0); h.setSpacing(8)
        lab = QLabel(title)
        h.addWidget(lab)
        if helpkey:
            h.addWidget(self._help_btn(helpkey))
        h.addStretch(1)
        h.addWidget(widget)
        return w

    def _btns(self, *items):
        w = QWidget(); h = QHBoxLayout(w); h.setContentsMargins(0, 6, 0, 0); h.addStretch(1)
        for label, cb, primary in items:
            b = QPushButton(label)
            if primary:
                b.setDefault(True)
            b.clicked.connect(cb)
            h.addWidget(b)
        return w

    def _section(self, title, helpkey=None):
        # ⚠️ Senza titolo nel QGroupBox: dentro l'intestazione di un QGroupBox
        # non ci si possono mettere altri widget, e il pallino «?» deve stare
        # accanto al nome della sezione. Il titolo diventa una riga propria con
        # lo stesso aspetto di prima.
        g = QGroupBox(); v = QVBoxLayout(g); v.setSpacing(8)
        riga = QHBoxLayout(); riga.setSpacing(6)
        lab = QLabel(title); lab.setStyleSheet("font-weight:700;")
        riga.addWidget(lab)
        if helpkey:
            riga.addWidget(self._help_btn(helpkey))
        riga.addStretch(1)
        v.addLayout(riga)
        return g, v

    def _run_async(self, fn, on_done, busy_msg):
        self.toast(busy_msg, 60)
        self._open_monitor(busy_msg)
        t = CmdThread(fn)
        t.done.connect(lambda res, th=t: (on_done(res), self._threads.remove(th) if th in self._threads else None))
        self._threads.append(t); t.start()

    # ---------- PRESET ----------
    def _presets(self):
        g, v = self._section(L("Preset", "Presets"))
        try:
            with open(PRESETS) as _pf:
                data = json.load(_pf)["presets"]
        except Exception:
            data = []
        # I preset si scelgono con un pallino e si applicano con UN bottone: cinque
        # «Applica» in colonna erano cinque occasioni di premere quello sbagliato,
        # e l'ultimo della fila e' «Crazy».
        self._preset_dati = data
        self._preset_grp = QButtonGroup(self)
        for i, p in enumerate(data):
            w = QWidget(); h = QHBoxLayout(w); h.setContentsMargins(0, 0, 0, 0)
            rb = QRadioButton(self._pname(p)); rb.setStyleSheet("font-weight:bold;")
            self._preset_grp.addButton(rb, i)
            h.addWidget(rb)
            # La descrizione sta dietro al «?», non sotto al nome: cinque righe
            # di spiegazione riempivano da sole il primo schermo della finestra.
            _d = self._pdesc(p)
            if _d:
                h.addWidget(Aiuto(_d, self._pname(p)))
            h.addStretch(1)
            v.addWidget(w)

        riga = QHBoxLayout(); riga.addStretch(1)
        self._preset_btn = QPushButton(L("Applica", "Apply"))
        # ⚠️ Spento finche' non si e' scelto: senza, il primo clic distratto
        # applicherebbe... niente, e chi guarda non capirebbe perche'.
        self._preset_btn.setEnabled(False)
        self._preset_grp.idToggled.connect(
            lambda _i, on: self._preset_btn.setEnabled(True) if on else None)
        self._preset_btn.clicked.connect(self._apply_preset_scelto)
        riga.addWidget(self._preset_btn)
        v.addLayout(riga)
        return g

    def _apply_preset_scelto(self):
        i = self._preset_grp.checkedId()
        if i < 0 or i >= len(self._preset_dati):
            return
        self._apply_preset(self._preset_dati[i])

    def _pname(self, p):
        return p.get("name_" + LANG) or p.get("name", "")

    def _pdesc(self, p):
        return p.get("desc_" + LANG) or p.get("desc", "")

    def _apply_preset(self, p):
        c = p["cpu"]
        if c["frequency"] >= 4000:
            # the Crazy-class presets are benchmark-only: 4000 @ heavy undervolt was
            # traced to repeated whole-machine hard-freezes when left applied 24/7
            if QMessageBox.warning(self, L("Preset estremo", "Extreme preset"),
                L("«%s» è pensato SOLO per sessioni di benchmark con ottimo raffreddamento.\n\nResta attivo anche dopo il riavvio e su alcune schede causa blocchi totali improvvisi (anche a riposo). Dopo i benchmark torna a Turbo o Performance.\n\nApplicare comunque?", "“%s” is meant ONLY for benchmark sessions with very good cooling.\n\nIt stays applied across reboots and on some boards causes sudden total freezes (even at idle). Switch back to Turbo or Performance after benchmarking.\n\nApply anyway?") % self._pname(p),
                QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
                QMessageBox.StandardButton.No) != QMessageBox.StandardButton.Yes:
                return
        self.d.cmd(cmd="apply-cpu", mhz=c["frequency"], scale=c["scale"], temp=c["max_temperature"])
        gg = p["gpu"]; self.d.cmd(cmd="apply-gpu", minmhz=gg["min_mhz"], minmv=gg["min_mv"], maxmhz=gg["max_mhz"], maxmv=gg["max_mv"])
        # ⚠️ Il preset NON tocca piu' la ventola: la curva e' di SkillFishOS Fan
        # Control, e scriverle sopra una percentuale fissa a ogni cambio di
        # preset era il secondo padrone sullo stesso PWM.
        if p.get("thermal_guard"):
            self.d.cmd(cmd="thermal-guard", limit=p["thermal_guard"])
        self.toast(L("Preset «%s» applicato", "Preset «%s» applied") % (self._pname(p),))

    # ---------- silicon status ----------
    def _silicon(self):
        g, v = self._section(L("Il mio silicio", "My silicon"))
        cpu = self.cfg.get("cpu", {}); gp = self.cfg.get("gpu", {}); cu = self.cfg.get("cu", {})
        freezes = []
        try:
            with open("/var/log/skillfish-freeze.log") as fh:
                freezes = [ln.strip() for ln in fh if ln.strip()]
        except OSError:
            pass  # no freezes recorded (or log unreadable): show 0
        lab = QLabel(L("CPU %d MHz @ %d mV  ·  GPU %d MHz @ %d mV  ·  CU attive: %d  ·  Blocchi rilevati: %d",
                       "CPU %d MHz @ %d mV  ·  GPU %d MHz @ %d mV  ·  Active CUs: %d  ·  Detected freezes: %d")
                     % (cpu.get("frequency", 0), round(cpu.get("scale", 0) * MV_PER_SCALINO),
                          gp.get("max_mhz", 0),
                        gp.get("max_mv", 0), cu.get("active", 0), len(freezes)))
        lab.setWordWrap(True); v.addWidget(lab)
        if freezes:
            warn = QLabel(L("⚠ Ultimo blocco: %s — se ricapita, scendi di uno scalino con i wizard.",
                            "⚠ Last freeze: %s — if it happens again, step down one notch with the wizards.")
                          % freezes[-1].split(" ")[0])
            warn.setStyleSheet("color:#e0a050;font-size:11px;"); warn.setWordWrap(True); v.addWidget(warn)
        v.addWidget(self._btns(
            (L("Log", "Log"), self._apri_giornale, False),
            (L("Condividi nel DB del silicio", "Share to the silicon DB"),
             lambda: self._share_silicon(validation="SkillFishOS Tuner - silicon status panel"), False),
        ))
        return g

    # ---------- CPU ----------
    def _cpu(self):
        g, v = self._section("CPU"); cpu = self.cfg.get("cpu", {})
        self.cf = Stepper(3500, 4500, cpu.get("frequency", 3700), "MHz")
        self.cs = Stepper(-50, 0, cpu.get("scale", 0), "", mostra=mv_undervolt)
        self.ct = Stepper(60, 95, cpu.get("max_temperature", 85), "°C")
        v.addWidget(self._row(L("Frequenza massima", "Max frequency"), self.cf, "cpu_freq"))
        v.addWidget(self._row(L("Undervolt", "Undervolt"), self.cs, "cpu_scale"))
        v.addWidget(self._row(L("Limite temperatura", "Temperature limit"), self.ct, "cpu_temp"))
        vals = lambda: (self.cf.value(), self.cs.value(), self.ct.value())
        v.addWidget(self._btns(
            (L("Trova il massimo", "Find my max"), self._cpu_wizard, False),
            (L("Suggerisci UV", "Suggest UV"), lambda: self._suggest_uv(self.cf.value()), False),
            (L("Test (benchmark)", "Test (benchmark)"), lambda: self._test_cpu(*vals()), False),
            (L("Applica", "Apply"), lambda: (self.d.cmd(cmd="apply-cpu", mhz=vals()[0], scale=vals()[1], temp=vals()[2]), self.toast(L("CPU applicata", "CPU applied"))), True),
            (L("Salva al boot", "Save at boot"), lambda: (self.d.cmd(cmd="persist-cpu", mhz=vals()[0], scale=vals()[1], temp=vals()[2]), self.toast(L("CPU salvata al boot", "CPU saved at boot"))), False),
        ))
        return g

    def _suggest_uv(self, mhz):
        self._run_async(lambda: self.d.cmd(cmd="suggest-uv", mhz=mhz), self._uv_done,
                        L("Cerco l'undervolt ottimale per %s MHz (può durare ~1 min)…", "Searching the optimal undervolt for %s MHz (may take ~1 min)…") % (mhz,))
    def _uv_done(self, r):
        s = r.get("suggested_scale", 0); self.cs.s.setValue(s)
        self.toast(L("Undervolt suggerito per %s MHz: scale %s (impostato, premi Applica)", "Suggested undervolt for %s MHz: scale %s (set, press Apply)") % (r.get('mhz', 0), s), 6)
    def _test_cpu(self, mhz, scale, tmp):
        self._run_async(lambda: self.d.cmd(cmd="test-cpu", mhz=mhz, scale=scale, temp=tmp),
                        self._cpu_done, L("Test CPU: applico e benchmark 60s (per temp realistica)…",
                                          "CPU test: applying and benchmarking 60s (for a realistic temp)…"))
    def _cpu_done(self, r):
        if r.get("ok"):
            b = r["bench"]; self.toast(L("✓ CPU OK e applicata — %s %s, %s°C", "✓ CPU OK and applied — %s %s, %s°C") % (b['score'], b['unit'], b['temp']), 6)
        else:
            self.toast(f"✗ {r.get('err', L('test fallito', 'test failed'))}", 7)

    # ---------- GPU ----------
    def _gpu(self):
        g, v = self._section("GPU"); gp = self.cfg.get("gpu", {})
        self.gf = Stepper(350, 2200, min(gp.get("max_mhz", 2200), 2200), "MHz")
        self.gv = Stepper(700, 1129, gp.get("max_mv", 1000), "mV")
        v.addWidget(self._row(L("Frequenza massima", "Max frequency"), self.gf, "gpu_freq"))
        v.addWidget(self._row(L("Voltaggio massimo", "Max voltage"), self.gv, "gpu_volt"))
        # governor mode: Balanced (default) / Performance
        self.gov_bal  = QPushButton(L("Bilanciato", "Balanced"))
        self.gov_perf = QPushButton(L("Performance", "Performance"))
        _gmg = QButtonGroup(self); _gmg.setExclusive(True)
        for _b in (self.gov_bal, self.gov_perf):
            _b.setCheckable(True); _b.setCursor(Qt.CursorShape.PointingHandCursor); _gmg.addButton(_b)
        (self.gov_perf if gp.get("gov_mode") == "performance" else self.gov_bal).setChecked(True)
        self.gov_bal.clicked.connect(lambda: (self.d.cmd(cmd="gov-mode", mode="balanced"),
            self.toast(L("Governor: Bilanciato", "Governor: Balanced"))))
        self.gov_perf.clicked.connect(lambda: (self.d.cmd(cmd="gov-mode", mode="performance"),
            self.toast(L("Governor: Performance — 2230 MHz sotto carico", "Governor: Performance — 2230 MHz under load"))))
        _gmw = QWidget(); _gmh = QHBoxLayout(_gmw); _gmh.setContentsMargins(0, 0, 0, 0); _gmh.setSpacing(6)
        _gmh.addWidget(self.gov_bal); _gmh.addWidget(self.gov_perf)
        v.addWidget(self._row(L("Modalità governor", "Governor mode"), _gmw, "gov_mode"))
        vals = lambda: (self.gf.value(), self.gv.value())
        v.addWidget(self._btns(
            (L("Trova il massimo", "Find my max"), self._gpu_wizard, False),
            (L("Test (benchmark)", "Test (benchmark)"), lambda: self._test_gpu(*vals()), False),
            (L("Applica", "Apply"), lambda: (self.d.cmd(cmd="apply-gpu", minmhz=350, minmv=700, maxmhz=vals()[0], maxmv=vals()[1]), self.toast(L("GPU applicata", "GPU applied"))), True),
        ))
        return g

    # ---------- find-my-max wizard (silicon lottery) ----------
    WIZ_STEPS = [2000, 2050, 2100, 2150, 2200]  # MHz, all at the validated 1000 mV ceiling

    def _gpu_wizard(self):
        if QMessageBox.question(self, L("Trova il massimo", "Find my max"),
            L("Il wizard prova la GPU a scalini crescenti (2000 → 2200 MHz a 1000 mV), "
              "validando ogni passo con un benchmark e tornando indietro se instabile.\n\n"
              "Durata ~2 minuti per passo, con la GPU sotto pieno carico.\n"
              "Al termine imposta il massimo stabile trovato per la TUA scheda.\n\nProcedere?",
              "The wizard steps the GPU up (2000 → 2200 MHz at 1000 mV), validating each "
              "step with a benchmark and rolling back if unstable.\n\n"
              "~2 minutes per step under full GPU load.\n"
              "When done it applies the highest stable point for YOUR board.\n\nProceed?")
            ) != QMessageBox.StandardButton.Yes:
            return
        self._wiz_queue = list(self.WIZ_STEPS)
        self._wiz_good = None
        self._wiz_bench = None
        self._wiz_next()

    def _wiz_next(self):
        if not self._wiz_queue:
            self._wiz_done(); return
        f = self._wiz_queue.pop(0)
        self._run_async(lambda f=f: self.d.cmd(cmd="test-gpu", minmhz=350, minmv=700, maxmhz=f, maxmv=1000),
                        lambda r, f=f: self._wiz_step(f, r),
                        L("Wizard: provo %d MHz…", "Wizard: testing %d MHz…") % f)

    def _wiz_step(self, f, r):
        if r.get("ok"):
            self._wiz_good = f; self._wiz_bench = r.get("bench")
            self.toast(L("✓ %d MHz stabile", "✓ %d MHz stable") % f, 4)
            self._wiz_next()
        else:
            self._wiz_done(failed_at=f)

    def _wiz_done(self, failed_at=None):
        if not self._wiz_good:
            self.toast(L("✗ Nemmeno 2000 MHz è stabile su questa scheda: resta il profilo attuale.",
                         "✗ Not even 2000 MHz is stable on this board: keeping the current profile."), 8)
            return
        self.gf.s.setValue(self._wiz_good); self.gv.s.setValue(1000)
        b = self._wiz_bench or {}
        extra = (" — %s %s, %s°C" % (b.get("score"), b.get("unit"), b.get("temp"))) if b.get("score") else ""
        msg = (L("Massimo stabile trovato: %d MHz @ 1000 mV%s (già applicato).",
                 "Highest stable point: %d MHz @ 1000 mV%s (already applied).") % (self._wiz_good, extra))
        if failed_at:
            msg += "\n" + L("(%d MHz non ha superato il test ed è stato annullato.)", "(%d MHz failed the test and was rolled back.)") % failed_at
        msg += "\n\n" + L("Vuoi condividere il risultato (anonimo) nel database della lotteria del silicio su GitHub?",
                          "Share the result (anonymous) in the silicon-lottery database on GitHub?")
        if QMessageBox.question(self, L("Risultato wizard", "Wizard result"), msg) == QMessageBox.StandardButton.Yes:
            self._share_silicon(gpu="%d @ 1000" % self._wiz_good,
                                validation="Tuner GPU find-my-max wizard (vkpeak per step)")

    def _share_silicon(self, cpu=None, gpu=None, validation=""):
        """Apre una DISCUSSIONE con i valori validati, gia' scritta.

        ⚠️ Discussioni e non piu' issue: una segnalazione del silicio non si
        risolve e non si chiude, quindi fra le cose da sistemare non ci sta.

        ⚠️ E il corpo lo scriviamo noi. Il modello a campi chiedeva kernel e
        versione all'utente, e nella prima segnalazione che ci e' arrivata quel
        campo era vuoto: se un dato ce l'ha la macchina, non si chiede a chi
        scrive. Restano fuori identificativi di ogni genere — solo frequenze,
        tensioni, CU e versioni. Chi preme il pulsante rilegge tutto nel browser
        prima di pubblicare.
        """
        import webbrowser, urllib.parse

        def leggi(cmd):
            try:
                return subprocess.run(cmd, shell=True, capture_output=True,
                                      text=True, timeout=5).stdout.strip() or "?"
            except Exception:
                return "?"

        cu = self.cfg.get("cu", {}).get("active", "?")
        corpo = (
            "### GPU max stable (MHz @ mV)\n%s\n\n"
            "### CPU max stable (MHz, undervolt offset if any)\n%s\n\n"
            "### Compute Units stable\n%s / 40\n\n"
            "### How was it validated?\n%s\n\n"
            "### Kernel\n%s\n\n"
            "### SkillFishOS Tuner\n%s\n\n"
            "### Cooling and notes\n_(fill in: stock blower, extra fan, case, "
            "temperatures, what failed at higher clocks)_\n"
        ) % (gpu or ("%d @ %d" % (self.gf.value(), self.gv.value())),
             cpu or ("%d @ %d" % (self.cf.value(), self.cs.value())),
             cu,
             validation or "SkillFishOS Tuner",
             leggi("uname -r"),
             leggi("dpkg-query -W -f='${Version}' skillfish-tuner 2>/dev/null"))
        fields = {"category": "silicon-lottery-users-db",
                  "title": "BC-250 stable clocks report",
                  "body": corpo}
        webbrowser.open("https://github.com/MTSistemi/SkillFishOS/discussions/new?"
                        + urllib.parse.urlencode(fields))

    # ---------- find-my-max wizard (CPU) ----------
    # frequency/undervolt ladder: each step is benchmarked and rolled back if it
    # throttles or fails; on disk the conf always holds the last-known-good value,
    # so even a hard freeze mid-test reboots into a stable config (watchdog-safe).
    CPU_WIZ_STEPS = [(3600, -8), (3700, -16), (3800, -20), (3900, -24), (4000, -36)]

    def _cpu_wizard(self):
        if QMessageBox.question(self, L("Trova il massimo (CPU)", "Find my max (CPU)"),
            L("Il wizard prova la CPU a scalini crescenti (3600 → 4000 MHz con undervolt "
              "progressivo), validando ogni passo con un benchmark e fermandosi al primo "
              "instabile.\n\n⚠ L'ultimo scalino può bloccare alcune schede: in tal caso il "
              "watchdog riavvia da solo e al boot torna l'ultimo valore stabile.\n\n"
              "Nota: il test valida la stabilità SOTTO CARICO; la stabilità a riposo "
              "(giorni di uso) la confermi usando il sistema.\n\nProcedere?",
              "The wizard steps the CPU up (3600 → 4000 MHz with progressive undervolt), "
              "validating each step with a benchmark and stopping at the first unstable "
              "one.\n\n⚠ The last step can freeze some boards: the watchdog then reboots "
              "automatically and the last stable value is restored at boot.\n\n"
              "Note: the test validates UNDER-LOAD stability; idle stability over days is "
              "confirmed by regular use.\n\nProceed?")
            ) != QMessageBox.StandardButton.Yes:
            return
        self._cwiz_queue = list(self.CPU_WIZ_STEPS)
        self._cwiz_good = None
        self._cwiz_next()

    def _cwiz_next(self):
        if not self._cwiz_queue:
            self._cwiz_done(); return
        f, s = self._cwiz_queue.pop(0)
        self._run_async(lambda f=f, s=s: self.d.cmd(cmd="test-cpu", mhz=f, scale=s, temp=85),
                        lambda r, f=f, s=s: self._cwiz_step(f, s, r),
                        L("Wizard CPU: provo %d MHz @ %d…", "CPU wizard: testing %d MHz @ %d…") % (f, s))

    def _cwiz_step(self, f, s, r):
        if r.get("ok"):
            self._cwiz_good = (f, s)
            self.toast(L("✓ %d MHz @ %d stabile", "✓ %d MHz @ %d stable") % (f, s), 4)
            self._cwiz_next()
        else:
            self._cwiz_done(failed_at=(f, s))

    def _cwiz_done(self, failed_at=None):
        if not self._cwiz_good:
            self.toast(L("✗ Nemmeno 3600 MHz è stabile: resta il profilo attuale.",
                         "✗ Not even 3600 MHz is stable: keeping the current profile."), 8)
            return
        f, s = self._cwiz_good
        self.cf.s.setValue(f); self.cs.s.setValue(s)
        msg = L("Massimo CPU stabile sotto carico: %d MHz @ undervolt %d (già applicato e salvato al boot).", "Highest load-stable CPU point: %d MHz @ undervolt %d (already applied and saved at boot).") % (f, s)
        if failed_at:
            msg += "\n" + L("(%d MHz @ %d non ha superato il test ed è stato annullato.)", "(%d MHz @ %d failed the test and was rolled back.)") % failed_at
        msg += "\n\n" + L("Consiglio: usa il sistema qualche giorno; se compare un blocco, scendi di uno scalino.\n\n"
                          "Vuoi condividere il risultato (anonimo) nel database della lotteria del silicio?",
                          "Tip: run the system for a few days; if a freeze appears, step down one notch.\n\n"
                          "Share the result (anonymous) in the silicon-lottery database?")
        if QMessageBox.question(self, L("Risultato wizard CPU", "CPU wizard result"), msg) == QMessageBox.StandardButton.Yes:
            self._share_silicon(cpu="%d @ %d" % (f, s),
                                validation="Tuner CPU find-my-max wizard (bench per step)")

    def _test_gpu(self, maxf, maxv):
        self._run_async(lambda: self.d.cmd(cmd="test-gpu", minmhz=350, minmv=700, maxmhz=maxf, maxmv=maxv),
                        self._gpu_done, L("Test GPU: applico e avvio vkpeak…", "GPU test: applying and running vkpeak…"))
    def _gpu_done(self, r):
        if r.get("ok"):
            b = r["bench"]; self.toast(L("✓ GPU OK e applicata — %s %s, %s°C", "✓ GPU OK and applied — %s %s, %s°C") % (b['score'], b['unit'], b['temp']), 6)
        else:
            self.toast(f"✗ {r.get('err', L('test fallito', 'test failed'))}", 7)

    # ---------- COMPUTE UNIT (live grid) ----------
    def _cu(self):
        g, v = self._section(L("Compute Unit (CU) — a caldo", "Compute Units (CU) — live"), "cu")
        cu = self.cfg.get("cu", {})
        self.cu_floor = int(cu.get("floor", 7))
        self.cu_order = ["0.0", "0.1", "1.0", "1.1"]
        rows = cu.get("rows") or {k: 7 for k in self.cu_order}
        self.cu_rows = {k: int(rows.get(k, 7)) | self.cu_floor for k in self.cu_order}
        grid = QGridLayout(); grid.setHorizontalSpacing(8); grid.setVerticalSpacing(5)
        for c in range(5):
            h = QLabel("WGP%d" % c); h.setStyleSheet("color:#8f7c52;font-size:10px;"); grid.addWidget(h, 0, c + 1)
        self.cu_cells = {}
        for r, rk in enumerate(self.cu_order):
            lab = QLabel("SE%s.SH%s" % (rk[0], rk[2])); lab.setStyleSheet("color:#b9a07a;font-size:11px;")
            grid.addWidget(lab, r + 1, 0)
            mask = self.cu_rows[rk]
            for wgp in range(5):
                locked = bool(self.cu_floor & (1 << wgp))
                cell = CuWgp(bool(mask & (1 << wgp)), locked,
                             (lambda on, rk=rk, wgp=wgp: self._cu_toggle(rk, wgp, on)))
                self.cu_cells[(rk, wgp)] = cell; grid.addWidget(cell, r + 1, wgp + 1)
        gw = QWidget(); gw.setLayout(grid); v.addWidget(gw)
        self.cu_count_lbl = QLabel(); self.cu_count_lbl.setStyleSheet("color:#d8a849;font-size:13px;")
        v.addWidget(self.cu_count_lbl); self._cu_update_count()
        pr = QHBoxLayout()
        for lbl, mk in (("24 CU", 0x07), ("32 CU", 0x0f), (L("40 CU (max)", "40 CU (max)"), 0x1f)):
            b = QPushButton(lbl); b.clicked.connect(lambda _=False, m=mk: self._cu_preset(m)); pr.addWidget(b)
        pr.addStretch(1)
        bt = QPushButton(L("Test CU", "Test CU")); bt.clicked.connect(self._test_cu); pr.addWidget(bt)
        ba = QPushButton(L("Applica", "Apply")); ba.setDefault(True); ba.clicked.connect(self._apply_cu); pr.addWidget(ba)
        pw = QWidget(); pw.setLayout(pr); v.addWidget(pw)
        return g

    def _sec_cpu_cores(self):
        g, v = self._section(L("Core CPU — a caldo", "CPU cores — live"), "cpucores")
        st = self.d.cmd(cmd="cpu-cores") or {}
        self.cpu_cores = st.get("cores", [])
        self.cpu_want = {c["core"]: c["online"] for c in self.cpu_cores}
        grid = QGridLayout(); grid.setHorizontalSpacing(8); grid.setVerticalSpacing(5)
        self.cpu_cells = {}
        for i, c in enumerate(self.cpu_cores):
            cell = CpuCore(c["core"], c["online"], not c.get("removable", True), self._cpu_toggle)
            self.cpu_cells[c["core"]] = cell
            grid.addWidget(cell, i // 8, i % 8)
        gw = QWidget(); gw.setLayout(grid); v.addWidget(gw)
        self.cpu_count_lbl = QLabel(); self.cpu_count_lbl.setStyleSheet("color:#d8a849;font-size:13px;")
        v.addWidget(self.cpu_count_lbl); self._cpu_update_count()
        pr = QHBoxLayout()
        for lbl, n in ((L("Tutti", "All"), 99), ("6 core", 6), ("4 core", 4)):
            b = QPushButton(lbl); b.clicked.connect(lambda _=False, k=n: self._cpu_preset(k)); pr.addWidget(b)
        self.smt_cb = QCheckBox("SMT")
        self.smt_cb.setChecked((st.get("smt") or "on") == "on")
        # Il testo sta in una variabile e va in DUE posti: il suggerimento al
        # passaggio del mouse (che c'era gia') e il pallino «?» (che mancava,
        # quindi la spiegazione la trovava solo chi ci finiva sopra per caso).
        aiuto_smt = L("Multi-threading: 2 thread per core. Spento = 1 thread per core.",
                      "Multi-threading: 2 threads per core. Off = 1 thread per core.")
        self.smt_cb.setToolTip(aiuto_smt)
        self.smt_cb.toggled.connect(self._cpu_smt)
        if not st.get("smt"): self.smt_cb.setEnabled(False)
        pr.addWidget(self.smt_cb)
        pr.addWidget(Aiuto(aiuto_smt, "SMT"))

        # Sblocco degli 8 core: spento di serie dal 16/08/2026. Sulla ISO
        # rilasciata partiva da solo e riavviava la scheda anche nella live,
        # uccidendo l'installazione a meta'. Qui l'utente sceglie, sapendo
        # quanto costa.
        cu = self.d.cmd(cmd="core-unlock") or {}
        self.core8_cb = QCheckBox(L("8 core", "8 cores"))
        self.core8_cb.setChecked(bool(cu.get("abilitato")))
        aiuto_core8 = L(
            "Sblocca i 2 core che la BC-250 tiene spenti (6c/12t diventa 8c/16t).\n\n"
            "Costa un riavvio in piu' a ogni accensione da spenta: e' il modo in cui\n"
            "la scheda rilegge quanti core ha. Ha effetto dal prossimo avvio.",
            "Unlocks the 2 cores the BC-250 keeps disabled (6c/12t becomes 8c/16t).\n\n"
            "Costs one extra reboot on every cold start: that is how the board\n"
            "re-reads how many cores it has. Takes effect from the next boot.")
        self.core8_cb.setToolTip(aiuto_core8)
        self.core8_cb.toggled.connect(self._core8)
        if not cu.get("supportato"): self.core8_cb.setEnabled(False)
        pr.addWidget(self.core8_cb)
        pr.addWidget(Aiuto(aiuto_core8, L("8 core", "8 cores")))
        pr.addStretch(1)
        ba = QPushButton(L("Applica", "Apply")); ba.clicked.connect(self._apply_cpu_cores); pr.addWidget(ba)
        pw = QWidget(); pw.setLayout(pr); v.addWidget(pw)
        return g

    def _cpu_toggle(self, core, on):
        self.cpu_want[core] = on; self._cpu_update_count()

    def _cpu_update_count(self):
        n = sum(1 for v in self.cpu_want.values() if v)
        thr = n * (2 if getattr(self, "smt_cb", None) is None or self.smt_cb.isChecked() else 1)
        self.cpu_count_lbl.setText(L("Core attivi: %d / %d  ·  %d thread", "Active cores: %d / %d  ·  %d threads")
                                   % (n, len(self.cpu_want) or 8, thr))

    def _cpu_preset(self, keep):
        for c in self.cpu_cores:
            want = True if keep >= len(self.cpu_cores) else (c["core"] < keep)
            if not c.get("removable", True): want = True      # core 0 stays
            self.cpu_want[c["core"]] = want
            cell = self.cpu_cells.get(c["core"])
            if cell and not cell.locked:
                cell.on = want; cell._paint()
        self._cpu_update_count()

    def _cpu_smt(self, on):
        res = self.d.cmd(cmd="cpu-smt", on=bool(on)) or {}
        if res.get("ok"):
            self.toast(L("SMT %s — %d thread", "SMT %s — %d threads")
                       % (L("acceso", "on") if on else L("spento", "off"), res.get("nproc", 0)), 4)
        else:
            self.toast(L("SMT: %s", "SMT: %s") % res.get("err", "errore"), 6)
        self._cpu_update_count()

    def _core8(self, on):
        res = self.d.cmd(cmd="core-unlock-set", on=bool(on)) or {}
        if res.get("ok"):
            self.toast(L("Sblocco 8 core %s. Ha effetto al prossimo riavvio.",
                         "8-core unlock %s. Takes effect on next reboot.")
                       % (L("acceso", "on") if on else L("spento", "off")), 6)
        else:
            self.toast(L("Sblocco 8 core: %s", "8-core unlock: %s")
                       % res.get("err", "errore"), 6)

    def _apply_cpu_cores(self):
        req = [{"core": k, "online": bool(v)} for k, v in self.cpu_want.items()]
        res = self.d.cmd(cmd="cpu-cores-set", cores=req) or {}
        if res.get("ok"):
            self.cpu_cores = res.get("cores", self.cpu_cores)
            self.toast(L("Core applicati — %d thread attivi", "Cores applied — %d active threads")
                       % res.get("nproc", 0), 5)
        else:
            self.toast(L("Errore core: %s", "Core error: %s") % res.get("err", "apply failed"), 7)

    def _cu_toggle(self, rk, wgp, on):
        m = self.cu_rows.get(rk, 7)
        m = (m | (1 << wgp)) if on else (m & ~(1 << wgp))
        self.cu_rows[rk] = m | self.cu_floor
        self._cu_update_count()

    def _cu_count(self):
        return sum(bin(self.cu_rows.get(rk, 7) | self.cu_floor).count("1") * 2 for rk in self.cu_order)

    def _cu_update_count(self):
        self.cu_count_lbl.setText(L("CU attive: %d / 40", "Active CUs: %d / 40") % self._cu_count())

    def _cu_preset(self, mask):
        mask |= self.cu_floor
        for rk in self.cu_order:
            self.cu_rows[rk] = mask
            for wgp in range(5):
                cell = self.cu_cells.get((rk, wgp))
                if cell and not cell.locked:
                    cell.on = bool(mask & (1 << wgp)); cell._paint()
        self._cu_update_count()

    def _apply_cu(self):
        rows = [self.cu_rows.get(rk, 7) | self.cu_floor for rk in self.cu_order]
        res = self.d.cmd(cmd="cu-apply", rows=rows)
        if res.get("ok"):
            self.toast(L("CU applicate: %s/40 (a caldo)", "CUs applied: %s/40 (live)") % res.get("active", "?"), 5)
        else:
            self.toast(L("Errore CU: %s", "CU error: %s") % res.get("err", "apply failed"), 7)

    def _test_cu(self):
        if QMessageBox.question(self, L("Test CU", "CU test"),
            L("Verifica una a una le CU extra (WGP3-4) per scovare difetti (lotteria del silicio): attiva ogni coppia da sola, la mette sotto sforzo con vkpeak e controlla errori/blocchi della GPU.\n\nDura ~2-3 minuti e la GPU sarà sotto carico. Procedere?",
              "Tests the extra CUs (WGP3-4) one by one to catch defects (silicon lottery): enables each pair alone, stresses it with vkpeak and checks for GPU errors/hangs.\n\nTakes ~2-3 minutes and the GPU will be under load. Proceed?")) != QMessageBox.StandardButton.Yes:
            return
        self._run_async(lambda: self.d.cmd(cmd="cu-test"), self._cu_test_done,
                        L("Test CU in corso (~2-3 min)…", "CU test running (~2-3 min)…"))

    def _cu_test_done(self, res):
        if not res.get("ok"):
            self.toast(L("Test CU fallito: %s", "CU test failed: %s") % res.get("err", "?"), 7); return
        sym = {"OK": "✓ OK", "FAIL": "✗ FAIL", "N/A": "— n/d"}
        f_err = res.get("full40_err", 0)
        lines = [L("40 CU sotto sforzo (vkpeak): %s GFLOPS%s",
                   "40 CU under load (vkpeak): %s GFLOPS%s") % (
                   res.get("full40", "?"),
                   (L(" · %d errori GPU", " · %d GPU errors") % f_err if f_err else "")),
                 L("Baseline 24 CU: %s GFLOPS", "Baseline 24 CU: %s GFLOPS") % res.get("baseline", "?"),
                 L("(atteso ~11300 a 40 CU)", "(expected ~11300 at 40 CU)"), "",
                 L("Stabilità per coppia di CU extra:", "Per extra-CU-pair stability:")]
        for r in res.get("results", []):
            lines.append("  SE%s.SH%s WGP%d (CU %s):  %s%s" % (
                r["row"][0], r["row"][2], r["wgp"], r["cu"], sym.get(r["verdict"], r["verdict"]),
                (" · %d err" % r["errors"]) if r["errors"] else ""))
        bad = res.get("bad", 0)
        lines.append("")
        if bad or f_err:
            lines.append(L("⚠ Rilevati problemi GPU sotto carico: possibile CU difettosa (lotteria del silicio). Valuta di tenere meno CU.",
                           "⚠ GPU problems under load detected: possible defective CU (silicon lottery). Consider keeping fewer CUs."))
        else:
            lines.append(L("✓ Nessun difetto: tutte le CU reggono lo sforzo senza errori GPU.",
                           "✓ No defects: all CUs sustain the load with no GPU errors."))
        QMessageBox.information(self, L("Risultato Test CU", "CU test result"), "\n".join(lines))

    # ---------- VRAM ----------
    def _vram(self):
        g, v = self._section("VRAM"); cur = self.cfg.get("vram", {}).get("uma_mb", 0)
        info = QLabel(L("Allocazione attuale: %d MB (%.1f GB)",
                        "Current allocation: %d MB (%.1f GB)") % (cur, cur / 1024.0))
        info.setStyleSheet("color:#d8a849;font-weight:bold;")
        hh = QHBoxLayout(); hw = QWidget(); hw.setLayout(hh); hh.setContentsMargins(0, 0, 0, 0)
        hh.addWidget(info); hh.addWidget(self._help_btn("vram")); hh.addStretch(1)
        v.addWidget(hw)
        self.vv = [2048, 3072, 4096, 6144, 8192, 10240, 12288]
        self.vram_combo = QComboBox()
        for mb in self.vv:
            self.vram_combo.addItem(f"{mb} MB ({mb/1024.0:.0f} GB)")
        try:
            self.vram_combo.setCurrentIndex(self.vv.index(cur))
        except Exception:
            self.vram_combo.setCurrentIndex(4)
        v.addWidget(self._row(L("Nuova allocazione", "New allocation"), self.vram_combo))
        v.addWidget(self._btns((L("Imposta (riavvio)", "Set (reboot)"), self._set_vram, False)))
        return g

    def _set_vram(self):
        mb = self.vv[self.vram_combo.currentIndex()]
        if QMessageBox.question(self, L("VRAM a %s MB?", "VRAM to %s MB?") % (mb,),
                                L("Scrive nel CMOS del BIOS. Serve un riavvio per applicare.",
                                  "Writes to the BIOS CMOS. A reboot is needed to apply.")) != QMessageBox.StandardButton.Yes:
            return
        self.d.cmd(cmd="set-vram", mb=mb); self.toast(L("VRAM %s MB — riavvia", "VRAM %s MB — reboot") % (mb,))


def main():
    app = QApplication(sys.argv)
    app.setApplicationName("SkillFishOS Tuner")
    app.setDesktopFileName("os.skillfish.Tuner")
    app.setWindowIcon(QIcon(ICON))
    daemon = Daemon()
    if not daemon.start():
        QMessageBox.critical(None, "SkillFishOS Tuner",
                             L("Autenticazione annullata o helper non avviato.\n"
                               "Riavvia il programma e autorizza l'operazione.",
                               "Authentication cancelled or helper not started.\n"
                               "Restart the program and authorize the operation."))
        sys.exit(1)
    w = TunerWindow(daemon); w.show()
    sys.exit(app.exec())


if __name__ == "__main__":
    main()
