#!/usr/bin/env python3
# SkillFishOS Kernel Manager — list every installed kernel, choose the boot
# kernel (permanent default / boot-once), and COMPLETELY uninstall kernels so
# they don't pile up. KDE-native PyQt6, brass/steampunk themed.
# Four languages: it / en / pl / uk, English being the fallback.
import sys, os, glob, subprocess
from PyQt6.QtCore import Qt
from PyQt6.QtGui import QIcon, QPixmap
from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
                             QLabel, QGroupBox, QPushButton, QRadioButton,
                             QButtonGroup, QMessageBox, QFrame, QScrollArea)

ICON = "/usr/share/icons/hicolor/256x256/apps/skillfish-kernel.png"
HELPER = "/usr/local/bin/skillfish-kernel-helper"

# ---------- 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 installed kernels. The running kernel cannot be removed.": "Zainstalowane jądra: %d. Działającego jądra nie można usunąć.",
    "%s will boot next time (once only). Reboot now?": "%s uruchomi się następnym razem (tylko raz). Uruchomić ponownie teraz?",
    "COMPLETELY uninstall the kernel:\n  %s": "CAŁKOWICIE odinstalować jądro:\n  %s",
    "Freed space: ~%.0f MB": "Zwolnione miejsce: ~%.0f MB",
    "Packages removed: %s (+ headers/dbg if present)": "Usunięte pakiety: %s (+ headers/dbg, jeśli są)",
    "✓ Default set to %s. Active on next reboot.": "✓ Domyślne ustawione na %s. Aktywne po ponownym uruchomieniu.",
    "✓ Kernel %s uninstalled (~%.0f MB freed).": "✓ Jądro %s odinstalowane (zwolniono ~%.0f MB).",

    "Generic · x86-64 · PCs & virtual machines": "Generic - x86-64 - PC i maszyny wirtualne",
    "Slim · BC-250 · ultra-lean kernel": "Slim - BC-250 - kernel minimalny",
    "BC-250 · znver2 · optimized": "BC-250 - znver2 - zoptymalizowany",
    "external kernel (non-SkillFishOS)": "kernel zewnętrzny (spoza SkillFishOS)",
    "SkillFishOS — Kernel Manager": "SkillFishOS — Menedżer Kerneli",
    "Kernel Manager": "Menedżer Kerneli",
    "Choose the boot kernel or remove the ones you don't need":
        "Wybierz kernel startowy lub usuń te, które są niepotrzebne",
    "Installed kernels": "Zainstalowane kernele",
    "Set as default": "Ustaw jako domyślny",
    "Boot once": "Uruchom raz",
    "Uninstall": "Odinstaluj",
    "Reboot now": "Uruchom ponownie",
    "running": "uruchomiony",
    "default": "domyślny",
    "You can't uninstall the running kernel. Boot another kernel and try again.":
        "Nie można odinstalować uruchomionego kernela. Uruchom inny kernel i spróbuj ponownie.",
    "It's the only installed kernel: it can't be removed.":
        "To jedyny zainstalowany kernel: nie można go usunąć.",
    "vmlinuz, initrd and the modules will be removed (not dpkg-managed).":
        "Zostaną usunięte vmlinuz, initrd i moduły (niezarządzane przez dpkg).",
    "\n⚠ It's the default kernel: the default will move to the running one.":
        "\n⚠ To domyślny kernel: domyślny zostanie ustawiony na uruchomiony.",
    "\nThis cannot be undone. Continue?": "\nTej operacji nie można cofnąć. Kontynuować?",
    "Uninstalling…": "Odinstalowywanie…",
    "operation cancelled": "operacja anulowana",
    "Reboot now?": "Uruchomić ponownie teraz?",
    "operation failed": "operacja nie powiodła się",
}

# 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.
# Stesso meccanismo del dizionario PL: la chiave e' la stringa inglese passata
# a L(), e una stringa non tradotta ricade sull'inglese invece di sparire.
UK = {
    # stringhe con segnaposto (il valore entra dopo la traduzione)
    "%d installed kernels. The running kernel cannot be removed.": "Встановлених ядер: %d. Активне ядро вилучити не можна.",
    "%s will boot next time (once only). Reboot now?": "%s завантажиться наступного разу (лише один раз). Перезавантажити зараз?",
    "COMPLETELY uninstall the kernel:\n  %s": "ПОВНІСТЮ вилучити ядро:\n  %s",
    "Freed space: ~%.0f MB": "Звільнено місця: ~%.0f МБ",
    "Packages removed: %s (+ headers/dbg if present)": "Вилучені пакунки: %s (+ headers/dbg, якщо є)",
    "✓ Default set to %s. Active on next reboot.": "✓ Типовим призначено %s. Діє після перезавантаження.",
    "✓ Kernel %s uninstalled (~%.0f MB freed).": "✓ Ядро %s вилучено (звільнено ~%.0f МБ).",

    "Generic · x86-64 · PCs & virtual machines": "Загальне · x86-64 · ПК та віртуальні машини",
    "Slim · BC-250 · ultra-lean kernel": "Полегшене · BC-250 · мінімальне ядро",
    "BC-250 · znver2 · optimized": "BC-250 · znver2 · оптимізоване",
    "external kernel (non-SkillFishOS)": "стороннє ядро (не SkillFishOS)",
    "SkillFishOS — Kernel Manager": "SkillFishOS — Менеджер ядер",
    "Kernel Manager": "Менеджер ядер",
    "Choose the boot kernel or remove the ones you don't need":
        "Виберіть ядро для завантаження або вилучіть непотрібні",
    "Installed kernels": "Встановлені ядра",
    "Set as default": "Зробити типовим",
    "Boot once": "Завантажити один раз",
    "Uninstall": "Вилучити",
    "Reboot now": "Перезавантажити зараз",
    "running": "запущене",
    "default": "типове",
    "You can't uninstall the running kernel. Boot another kernel and try again.":
        "Не можна вилучити запущене ядро. Завантажте інше ядро і спробуйте ще раз.",
    "It's the only installed kernel: it can't be removed.":
        "Це єдине встановлене ядро: його не можна вилучити.",
    "vmlinuz, initrd and the modules will be removed (not dpkg-managed).":
        "vmlinuz, initrd і модулі буде вилучено (не через dpkg).",
    "\n⚠ It's the default kernel: the default will move to the running one.":
        "\n⚠ Це типове ядро: типовим стане запущене.",
    "\nThis cannot be undone. Continue?": "\nЦю дію не можна скасувати. Продовжити?",
    "Uninstalling…": "Вилучення…",
    "operation cancelled": "операцію скасовано",
    "Reboot now?": "Перезавантажити зараз?",
    "operation failed": "операція не вдалася",
}


def L(it, en):
    """Restituisce la stringa in it, pl, uk o en secondo il locale rilevato.

    Le due stringhe che arrivano qui devono essere COSTANTI: se il testo viene
    formattato prima (una f-string, o un "%s" % x), la chiave cercata nei
    dizionari contiene gia' il valore e non la si trova mai. Il segnaposto va
    lasciato dentro e riempito dopo: L("Kernel %s", "Kernel %s") % nome.
    """
    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)

def sh(cmd, t=30):
    try:
        r = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=t)
        return r.returncode, (r.stdout or "").strip(), (r.stderr or "").strip()
    except Exception as e:
        return 1, "", str(e)

def running_kernel():
    return os.uname().release

def grub_default_raw():
    try:
        with open("/etc/default/grub") as f:
            for line in f:
                if line.startswith("GRUB_DEFAULT="):
                    return line.split("=", 1)[1].strip().strip('"')
    except Exception:
        pass  # best-effort: grub config may be unreadable
    return ""

def flavor(kv):
    if kv.endswith("-generic"):
        return L("Generic · x86-64 · PC e macchine virtuali", "Generic · x86-64 · PCs & virtual machines")
    if kv.endswith("-slim"):
        return L("Slim · BC-250 · kernel minimale", "Slim · BC-250 · ultra-lean kernel")
    if "skillfishos" in kv:
        return L("BC-250 · znver2 · ottimizzato", "BC-250 · znver2 · optimized")
    return L("kernel esterno (non SkillFishOS)", "external kernel (non-SkillFishOS)")

def is_skillfish(kv):
    return "skillfishos" in kv

def kernel_pkg(kv):
    rc, out, _ = sh("dpkg-query -S /boot/vmlinuz-%s 2>/dev/null" % kv)
    if rc == 0 and ":" in out:
        return out.split(":", 1)[0].split(",")[0].strip()
    return None

def kernel_size_mb(kv):
    total = 0
    for p in ("/boot/vmlinuz-%s" % kv, "/boot/initrd.img-%s" % kv,
              "/boot/System.map-%s" % kv, "/boot/config-%s" % kv):
        try:
            total += os.path.getsize(p)
        except OSError:
            pass  # best-effort: some files may be absent
    md = "/usr/lib/modules/%s" % kv
    if not os.path.isdir(md):
        md = "/lib/modules/%s" % kv
    rc, out, _ = sh("du -sb %s 2>/dev/null" % md)
    try:
        total += int(out.split()[0])
    except (ValueError, IndexError):
        pass  # best-effort: modules dir may be missing
    return total / 1e6

def installed_kernels():
    ks = []
    for p in glob.glob("/boot/vmlinuz-*"):
        kv = os.path.basename(p).replace("vmlinuz-", "")
        if kv.endswith((".dpkg-tmp", ".old")):
            continue
        ks.append(kv)
    return sorted(set(ks), reverse=True)


class KernelManager(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle(L("SkillFishOS — Gestore Kernel", "SkillFishOS — Kernel Manager"))
        self.setWindowIcon(QIcon(ICON))
        self.resize(680, 600)
        w = QWidget(); self.setCentralWidget(w)
        v = QVBoxLayout(w); v.setContentsMargins(22, 18, 22, 18); v.setSpacing(14)

        # header
        h = QHBoxLayout()
        img = QLabel(); pm = QPixmap(ICON)
        if not pm.isNull():
            img.setPixmap(pm.scaled(40, 40, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation))
        h.addWidget(img)
        tcol = QVBoxLayout(); tcol.setSpacing(0)
        title = QLabel(L("Gestore Kernel", "Kernel Manager")); title.setStyleSheet("font-size:20px;font-weight:800;color:#e8c878;")
        sub = QLabel(L("Scegli il kernel di avvio o rimuovi quelli che non servono",
                       "Choose the boot kernel or remove the ones you don't need"))
        sub.setStyleSheet("color:#b9a07a;")
        tcol.addWidget(title); tcol.addWidget(sub)
        h.addLayout(tcol); h.addStretch(1); v.addLayout(h)

        # scrollable kernel list
        self.box = QGroupBox(L("Kernel installati", "Installed kernels"))
        bv = QVBoxLayout(self.box); bv.setSpacing(6)
        self.listhost = QWidget(); self.listlay = QVBoxLayout(self.listhost)
        self.listlay.setContentsMargins(0, 0, 0, 0); self.listlay.setSpacing(6)
        scroll = QScrollArea(); scroll.setWidgetResizable(True); scroll.setFrameShape(QFrame.Shape.NoFrame)
        scroll.setWidget(self.listhost); bv.addWidget(scroll)
        v.addWidget(self.box, 1)

        self.status = QLabel(""); self.status.setWordWrap(True); self.status.setStyleSheet("color:#b9a07a;")
        v.addWidget(self.status)

        b = QHBoxLayout()
        self.bdef = QPushButton(L("Imposta come predefinito", "Set as default")); self.bdef.clicked.connect(self.set_default)
        self.bonce = QPushButton(L("Avvia una volta", "Boot once")); self.bonce.clicked.connect(self.boot_once)
        self.bdel = QPushButton(L("Disinstalla", "Uninstall")); self.bdel.clicked.connect(self.uninstall)
        self.bdel.setStyleSheet("QPushButton{color:#e0b0a0;} QPushButton:hover{color:#ff9a7a;}")
        self.breb = QPushButton(L("Riavvia ora", "Reboot now")); self.breb.clicked.connect(self.reboot)
        b.addWidget(self.bdel); b.addStretch(1)
        for x in (self.bdef, self.bonce, self.breb): b.addWidget(x)
        v.addLayout(b)
        self.statusBar()

        self.grp = QButtonGroup(self)
        self.reload()

    # ---- build / refresh the kernel rows ----
    def reload(self):
        # clear old rows
        for btn in list(self.grp.buttons()):
            self.grp.removeButton(btn)
        while self.listlay.count():
            it = self.listlay.takeAt(0)
            wdg = it.widget()
            if wdg is not None:
                wdg.setParent(None)

        self.run = running_kernel()
        self.default_raw = grub_default_raw()
        self.kernels = installed_kernels()
        for i, kv in enumerate(self.kernels):
            row = QFrame(); rl = QHBoxLayout(row); rl.setContentsMargins(6, 4, 6, 4)
            rb = QRadioButton(); rb.setProperty("kver", kv); self.grp.addButton(rb, i)
            col = QVBoxLayout(); col.setSpacing(0)
            tags = []
            if kv == self.run: tags.append("● " + L("in esecuzione", "running"))
            if kv and kv in self.default_raw: tags.append("★ " + L("predefinito", "default"))
            nm = QLabel(kv + (("   " + "  ·  ".join(tags)) if tags else ""))
            running_or_default = (kv == self.run)
            nm.setStyleSheet("font-weight:700;color:%s" % ("#9ccf6a" if running_or_default else "#f1e3c6"))
            meta = QLabel("%s   ·   ~%.0f MB%s" % (
                flavor(kv), kernel_size_mb(kv),
                "" if is_skillfish(kv) else "   ·   ⚠"))
            meta.setStyleSheet("color:#b9a07a;")
            col.addWidget(nm); col.addWidget(meta)
            rl.addWidget(rb); rl.addLayout(col); rl.addStretch(1)
            self.listlay.addWidget(row)
            if kv == self.run:
                rb.setChecked(True)
        self.listlay.addStretch(1)
        n = len(self.kernels)
        self.status.setText(L("%d kernel installati. Il kernel in esecuzione non è rimovibile.", "%d installed kernels. The running kernel cannot be removed.") % n)
        self.status.setStyleSheet("color:#b9a07a;")
        # uninstall is only possible when there is more than one kernel
        self.bdel.setEnabled(n > 1)

    def _selected(self):
        btn = self.grp.checkedButton()
        return btn.property("kver") if btn else None

    # ---- actions ----
    def set_default(self):
        kv = self._selected()
        if not kv: return
        rc, out, err = sh(f"pkexec {HELPER} default {kv}", 120)
        if rc == 0:
            self._ok(L("✓ Predefinito impostato a %s. Attivo al prossimo riavvio.", "✓ Default set to %s. Active on next reboot.") % (kv,))
            self.reload()
        else:
            self._err(out or err)

    def boot_once(self):
        kv = self._selected()
        if not kv: return
        rc, out, err = sh(f"pkexec {HELPER} once {kv}", 90)
        if rc == 0:
            if QMessageBox.question(self, "SkillFishOS",
                    L("%s verrà avviato al prossimo riavvio (solo una volta). Riavviare ora?", "%s will boot next time (once only). Reboot now?") % (kv,)) == QMessageBox.StandardButton.Yes:
                sh("pkexec systemctl reboot")
        else:
            self._err(out or err)

    def uninstall(self):
        kv = self._selected()
        if not kv: return
        if kv == self.run:
            QMessageBox.warning(self, "SkillFishOS",
                L("Non puoi disinstallare il kernel in esecuzione. Avvia un altro kernel e riprova.",
                  "You can't uninstall the running kernel. Boot another kernel and try again."))
            return
        if len(self.kernels) <= 1:
            QMessageBox.warning(self, "SkillFishOS",
                L("È l'unico kernel installato: non può essere rimosso.",
                  "It's the only installed kernel: it can't be removed."))
            return
        pkg = kernel_pkg(kv); size = kernel_size_mb(kv)
        is_def = kv in self.default_raw
        lines = [L("Disinstallare COMPLETAMENTE il kernel:\n  %s", "COMPLETELY uninstall the kernel:\n  %s") % (kv,), ""]
        if pkg:
            lines.append(L("Pacchetti rimossi: %s (+ headers/dbg se presenti)", "Packages removed: %s (+ headers/dbg if present)") % (pkg,))
        else:
            lines.append(L("Verranno rimossi vmlinuz, initrd e i moduli (non gestito da dpkg).",
                           "vmlinuz, initrd and the modules will be removed (not dpkg-managed)."))
        lines.append(L("Spazio liberato: ~%.0f MB", "Freed space: ~%.0f MB") % size)
        if is_def:
            lines.append(L("\n⚠ È il kernel predefinito: il default tornerà a quello in esecuzione.",
                           "\n⚠ It's the default kernel: the default will move to the running one."))
        lines.append(L("\nL'operazione è irreversibile. Continuare?",
                       "\nThis cannot be undone. Continue?"))
        m = QMessageBox(self); m.setIcon(QMessageBox.Icon.Warning)
        m.setWindowTitle("SkillFishOS"); m.setText("\n".join(lines))
        m.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
        m.setDefaultButton(QMessageBox.StandardButton.No)
        if m.exec() != QMessageBox.StandardButton.Yes:
            return
        self.bdel.setEnabled(False); self.status.setText(L("Disinstallazione in corso…", "Uninstalling…"))
        QApplication.processEvents()
        rc, out, err = sh(f"pkexec {HELPER} uninstall {kv}", 600)
        if rc == 0:
            self._ok(L("✓ Kernel %s disinstallato (~%.0f MB liberati).",
                       "✓ Kernel %s uninstalled (~%.0f MB freed).") % (kv, size))
        else:
            self._err(out or err or L("operazione annullata", "operation cancelled"))
        self.reload()

    def reboot(self):
        if QMessageBox.question(self, "SkillFishOS", L("Riavviare ora?", "Reboot now?")) == QMessageBox.StandardButton.Yes:
            sh("pkexec systemctl reboot")

    def _ok(self, msg):
        self.status.setText(msg); self.status.setStyleSheet("color:#9ccf6a;font-weight:600;")

    def _err(self, msg):
        self.status.setText(msg or L("operazione fallita", "operation failed"))
        self.status.setStyleSheet("color:#cf8a6a;")


def main():
    app = QApplication(sys.argv)
    app.setApplicationName("SkillFishOS Kernel Manager")
    app.setDesktopFileName("os.skillfish.kernel")
    app.setWindowIcon(QIcon(ICON))
    w = KernelManager()
    w.show(); w.raise_(); w.activateWindow()
    sys.exit(app.exec())

if __name__ == "__main__":
    main()
