#!/usr/bin/env python3
#--------------------------------------------------------------------------------------------------------
# Name: Linux Lite - Lite Core
# Architecture: amd64
# Author: Jerry Bezencon
# Website: https://www.linuxliteos.com
# Language: Python/GTK4
# Licence: GPLv2
#--------------------------------------------------------------------------------------------------------

import gi
gi.require_version('Gtk', '4.0')
gi.require_version('Adw', '1')
from gi.repository import Gtk, Adw, Gio, GLib, Gdk

import subprocess
import os
import sys
import threading

import gettext as _gt, locale as _loc
TEXTDOMAIN = "lite-core"
ENV_FILE = "/tmp/.lite-core-env"
# When re-launched as root via pkexec, LANG/LANGUAGE are scrubbed; main() saves
# them to ENV_FILE before elevating and we restore them here, before gettext
# binds and before REMOVABLE_PACKAGES' _() calls run.
if os.geteuid() == 0 and os.path.exists(ENV_FILE):
    try:
        with open(ENV_FILE) as _ef:
            for _line in _ef:
                _line = _line.strip()
                if "=" in _line:
                    _k, _v = _line.split("=", 1)
                    if _k in ("LANG", "LANGUAGE", "LC_ALL", "LC_MESSAGES", "LC_NUMERIC"):
                        os.environ[_k] = _v
    except Exception:
        pass
try:
    _loc.setlocale(_loc.LC_ALL, "")
except _loc.Error:
    pass
_gt.bindtextdomain(TEXTDOMAIN, "/usr/share/locale")
_gt.textdomain(TEXTDOMAIN)
_ = _gt.translation(TEXTDOMAIN, "/usr/share/locale", fallback=True).gettext

APP_ID = "com.linuxlite.core"
APP_NAME = "Lite Core"
ICON_NAME = "lite-core"

# Hidden packages — removed silently during cleanup (glob patterns supported)
HIDDEN_PACKAGES = [
    "default-jre-headless",
    "libobasis*",
    "openjdk*",
]

# Desktop files in ~/.local/share/applications/ to remove per app
DESKTOP_FILE_CLEANUP = {
    "Blueman": ["blueman-manager.desktop"],
    "Evince": ["evince.desktop", "org.gnome.Evince.desktop"],
    "GNU Info": ["info.desktop"],
    "GNOME Disks": ["org.gnome.DiskUtility.desktop"],
    "GNOME Paint": ["gnome-paint.desktop"],
    "GNOME System Log": ["gnome-system-log.desktop"],
    "GParted": ["gparted.desktop"],
    "LibreOffice": ["libreoffice*.desktop"],
    "LightDM Settings": ["lightdm-gtk-greeter-settings.desktop"],
    "Onboard": ["onboard-settings.desktop"],
    "Orca": ["orca-settingsll.desktop"],
    "Shotwell": [
        "shotwell.desktop",
        "org.gnome.Shotwell.desktop",
        "org.gnome.Shotwell-Profile-Browser.desktop",
        "org.gnome.Shotwell-Viewer.desktop",
    ],
    "Simple Scan": ["simple-scan.desktop"],
    "Timeshift": ["timeshift-gtk.desktop"],
    "Xfburn": ["xfburn.desktop"],
}

# Packages to remove — each entry: (display_name, icon_name, package_names, description)
REMOVABLE_PACKAGES = [
    ("Blueman", "blueman", "blueman", _("Bluetooth manager")),
    ("Deja Dup", "deja-dup", "deja-dup", _("Backup tool")),
    ("Evince", "evince", "evince", _("Document viewer")),
    ("GIMP", "gimp", "gimp gimp-data", _("GNU Image Manipulation Program")),
    ("GNOME Disks", "gnome-disks", "gnome-disk-utility", _("Disk management utility")),
    ("GNOME Font Viewer", "org.gnome.font-viewer", "gnome-font-viewer", _("Font viewer")),
    ("GNOME Paint", "gnome-paint", "gnome-paint", _("Simple drawing application")),
    ("GNU Info", "dialog-information", "info", _("GNU info document viewer")),
    ("GNOME System Log", "utilities-log-viewer", "gnome-system-log", _("System log viewer")),
    ("GParted", "gparted", "gparted", _("Partition editor")),
    ("Hardinfo2", "hardinfo2", "hardinfo2", _("System information tool")),
    ("LibreOffice", "libreoffice-startcenter",
     "libreoffice-writer libreoffice-calc libreoffice-impress libreoffice-draw "
     "libreoffice-math libreoffice-base libreoffice-common libreoffice-core",
     _("Office productivity suite")),
    ("LightDM Settings", "lightdm-gtk-greeter-settings",
     "lightdm-gtk-greeter-settings lightdm-settings", _("Login screen settings")),
    ("Mintstick", "mintstick", "mintstick", _("USB image writer and formatter")),
    ("Mousepad", "mousepad", "mousepad", _("Text editor")),
    ("Onboard", "onboard", "onboard", _("On-screen keyboard")),
    ("Orca", "orca", "orca", _("Screen reader")),
    # NOTE: Samba is intentionally NOT offered here. The lite-software package
    # (which ships Lite Core itself, plus Lite Share Folder etc.) Depends on
    # samba, so `apt-get purge -y samba` would drag the whole Lite suite out
    # with it. There is no safe way to remove it from this tool.
    ("Shotwell", "shotwell", "shotwell", _("Photo manager")),
    ("Simple Scan", "org.gnome.SimpleScan", "simple-scan", _("Document scanner")),
    ("Thunderbird", "thunderbird", "thunderbird", _("Email client")),
    ("Timeshift", "timeshift", "timeshift", _("System backup and restore")),
    ("VLC", "vlc", "vlc vlc-data vlc-plugin-base", _("Media player")),
    ("Xfburn", "xfburn", "xfburn", _("CD/DVD burning tool")),
    ("Xfce4 Screenshooter", "xfce4-screenshooter", "xfce4-screenshooter", _("Screenshot tool")),
]


def get_package_status(packages):
    """Check if the main package is installed."""
    pkg = packages.split()[0]
    try:
        result = subprocess.run(
            ["dpkg-query", "-W", "-f=${Status}", pkg],
            capture_output=True, text=True
        )
        if result.returncode == 0 and "install ok installed" in result.stdout:
            return True
    except FileNotFoundError:
        pass
    return False


# Packages that must never be removed as collateral damage. Removing any of
# these would cripple the Lite suite — Lite Core itself ships in lite-software,
# which Depends on samba, so a naive `purge` could otherwise take the whole
# management stack out (this exact thing happened: purging Samba removed
# lite-software entirely). Before purging anything we simulate the transaction
# and bail if a protected package would be dragged along.
PROTECTED_EXACT = {"lite-software"}
PROTECTED_PREFIXES = ("lite-", "linuxlite")


def purge_would_remove_protected(pkg_list):
    """Dry-run `apt-get purge` for pkg_list; return the protected packages apt
    would ALSO remove (empty list means the purge is safe). Forces C locale so
    the simulation output is parsed in English regardless of UI language."""
    try:
        sim = subprocess.run(
            ["apt-get", "-s", "purge"] + pkg_list,
            capture_output=True, text=True, timeout=120,
            env={**os.environ, "LANG": "C.UTF-8", "LC_ALL": "C.UTF-8"},
        )
    except Exception:
        # If we can't simulate, treat it as unsafe rather than risk a blind
        # destructive purge.
        return ["<simulation-failed>"]
    hits = []
    for line in sim.stdout.splitlines():
        line = line.strip()
        if line.startswith("Remv "):
            name = line.split()[1]
            if name in PROTECTED_EXACT or name.startswith(PROTECTED_PREFIXES):
                hits.append(name)
    return hits


class LiteCoreWindow(Adw.ApplicationWindow):
    """Main application window."""

    def __init__(self, app):
        super().__init__(application=app)
        self.set_title(APP_NAME)
        self.set_default_size(520, 600)
        self.set_resizable(False)
        self.set_icon_name(ICON_NAME)

        # Track running operation
        self._running = False

        # Main layout
        main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
        self.set_content(main_box)

        # Header bar
        header = Adw.HeaderBar()
        main_box.append(header)

        # Scrollable content area
        scrolled = Gtk.ScrolledWindow()
        scrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
        scrolled.set_vexpand(True)
        main_box.append(scrolled)

        clamp = Adw.Clamp()
        clamp.set_maximum_size(480)
        clamp.set_margin_top(20)
        clamp.set_margin_bottom(20)
        clamp.set_margin_start(20)
        clamp.set_margin_end(20)
        scrolled.set_child(clamp)

        content_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=16)
        clamp.set_child(content_box)

        # Description
        desc = Gtk.Label(
            label=_("Strip your Linux Lite installation down to the essentials.\n"
                  "Select the applications you want to remove.")
        )
        desc.set_wrap(True)
        desc.set_xalign(0.5)
        desc.set_justify(Gtk.Justification.CENTER)
        desc.add_css_class("dim-label")
        content_box.append(desc)

        # Package list group
        self.pkg_group = Adw.PreferencesGroup()
        self.pkg_group.set_title(_("Applications"))
        content_box.append(self.pkg_group)

        # Select All row
        select_all_row = Adw.ActionRow()
        select_all_row.set_title(_("Select All"))
        self.select_all_check = Gtk.CheckButton()
        self.select_all_check.set_valign(Gtk.Align.CENTER)
        self.select_all_check.connect("toggled", self._on_select_all_toggled)
        select_all_row.add_suffix(self.select_all_check)
        select_all_row.set_activatable_widget(self.select_all_check)
        self.pkg_group.add(select_all_row)

        # Build rows with checkboxes
        self.check_rows = []
        for display_name, icon_name, packages, description in REMOVABLE_PACKAGES:
            installed = get_package_status(packages)
            row = Adw.ActionRow()
            row.set_title(display_name)
            if installed:
                row.set_subtitle(description)
            else:
                row.set_subtitle(_("{description}  —  not installed").format(description=description))
            row.set_activatable(installed)

            icon = Gtk.Image.new_from_icon_name(icon_name)
            icon.set_pixel_size(24)
            row.add_prefix(icon)

            check = Gtk.CheckButton()
            check.set_sensitive(installed)
            check.set_valign(Gtk.Align.CENTER)
            check.connect("toggled", self._on_check_toggled)
            row.add_suffix(check)
            row.set_activatable_widget(check)

            self.check_rows.append((display_name, packages, description, row, check, installed))
            self.pkg_group.add(row)

        # Progress bar (hidden initially)
        self.progress_bar = Gtk.ProgressBar()
        self.progress_bar.set_show_text(True)
        self.progress_bar.set_visible(False)
        content_box.append(self.progress_bar)

        # Status label (hidden initially)
        self.status_label = Gtk.Label()
        self.status_label.set_wrap(True)
        self.status_label.set_xalign(0)
        self.status_label.add_css_class("dim-label")
        self.status_label.set_visible(False)
        content_box.append(self.status_label)

        # Remove button
        self.remove_btn = Gtk.Button(label=_("Remove Selected"))
        self.remove_btn.add_css_class("destructive-action")
        self.remove_btn.add_css_class("pill")
        self.remove_btn.set_halign(Gtk.Align.CENTER)
        self.remove_btn.set_size_request(200, -1)
        self.remove_btn.set_sensitive(False)
        self.remove_btn.connect("clicked", self._on_remove_clicked)
        content_box.append(self.remove_btn)

    def _on_check_toggled(self, checkbox):
        """Update Remove button sensitivity based on selections."""
        any_selected = any(
            check.get_active() and installed
            for name, pkgs, desc, row, check, installed in self.check_rows
        )
        self.remove_btn.set_sensitive(any_selected)

    def _on_select_all_toggled(self, checkbox):
        """Toggle all installed package checkboxes."""
        active = checkbox.get_active()
        for name, pkgs, desc, row, check, installed in self.check_rows:
            if installed:
                check.set_active(active)

    def _on_remove_clicked(self, button):
        """Gather selected packages and confirm removal."""
        selected = [
            (name, pkgs)
            for name, pkgs, desc, row, check, installed in self.check_rows
            if check.get_active() and installed
        ]

        if not selected:
            dialog = Adw.AlertDialog()
            dialog.set_heading(_("No Selection"))
            dialog.set_body(_("Please select at least one application to remove."))
            dialog.add_response("ok", _("OK"))
            dialog.present(self)
            return

        self._present_confirm_dialog(selected)

    def _present_confirm_dialog(self, selected):
        names = ", ".join(n for n, _ in selected)
        dialog = Adw.AlertDialog()
        dialog.set_heading(_("Confirm Removal"))
        dialog.set_body(_("The following will be removed:\n\n{names}\n\nThis cannot be undone.").format(names=names))
        dialog.add_response("cancel", _("Cancel"))
        dialog.add_response("remove", _("Remove"))
        dialog.set_response_appearance("remove", Adw.ResponseAppearance.DESTRUCTIVE)
        dialog.set_default_response("cancel")
        dialog.set_close_response("cancel")
        dialog.connect("response", self._on_confirm_response, selected)
        dialog.present(self)

    def _on_confirm_response(self, dialog, response, selected):
        """Handle confirmation dialog response."""
        if response != "remove":
            return
        self._run_removal(selected)

    def _run_removal(self, selected):
        """Remove selected packages in a background thread."""
        self._running = True
        self.remove_btn.set_sensitive(False)
        self.progress_bar.set_visible(True)
        self.status_label.set_visible(True)

        # Collect all package names
        all_pkgs = []
        for name, pkgs in selected:
            all_pkgs.extend(pkgs.split())

        total = len(selected)

        def worker():
            removed = []
            failed = []

            for i, (name, pkgs) in enumerate(selected):
                GLib.idle_add(self.status_label.set_label, _("Removing {name}...").format(name=name))
                GLib.idle_add(self.progress_bar.set_fraction, i / total)
                GLib.idle_add(
                    self.progress_bar.set_text,
                    _("{n} of {total}").format(n=i + 1, total=total)
                )

                pkg_list = pkgs.split()

                # Safety net: never let a purge cascade into the Lite suite
                # itself. If apt would also remove a protected package, skip
                # this entry rather than self-destruct.
                protected = purge_would_remove_protected(pkg_list)
                if protected:
                    failed.append(name)
                    continue

                try:
                    result = subprocess.run(
                        ["apt-get", "purge", "-y"] + pkg_list,
                        capture_output=True, text=True, timeout=300
                    )
                    if result.returncode == 0:
                        removed.append(name)
                    else:
                        failed.append(name)
                except Exception:
                    failed.append(name)

            # Remove associated desktop files for ALL uninstalled apps
            # Clean from both /usr/share/applications and ~/.local/share/applications
            import glob as _glob
            uid = os.environ.get("PKEXEC_UID") or os.environ.get("SUDO_UID")
            if uid:
                import pwd
                user_home = pwd.getpwuid(int(uid)).pw_dir
            else:
                user_home = os.path.expanduser("~")
            cleanup_dirs = [
                "/usr/share/applications",
                os.path.join(user_home, ".local", "share", "applications"),
            ]
            for app_name, desktop_files in DESKTOP_FILE_CLEANUP.items():
                # Find the package string for this app
                pkg_str = None
                for dname, _unused1, pkgs, _unused2 in REMOVABLE_PACKAGES:
                    if dname == app_name:
                        pkg_str = pkgs
                        break
                # If the app is not installed, remove its desktop files
                if pkg_str and not get_package_status(pkg_str):
                    for apps_dir in cleanup_dirs:
                        for pattern in desktop_files:
                            for desktop_path in _glob.glob(
                                os.path.join(apps_dir, pattern)
                            ):
                                try:
                                    os.remove(desktop_path)
                                except OSError:
                                    pass

            # Purge hidden packages
            GLib.idle_add(self.status_label.set_label, _("Cleaning up..."))
            GLib.idle_add(self.progress_bar.set_fraction, 0.85)
            hidden_cmd = "apt-get purge -y " + " ".join(HIDDEN_PACKAGES)
            subprocess.run(
                hidden_cmd, shell=True,
                capture_output=True, text=True, timeout=300
            )

            # Autoremove leftover dependencies. `--purge` is essential —
            # plain `autoremove` keeps conffiles, and apt-hook-shipping
            # packages (e.g. ubuntu-helper-virt-hwe with its
            # /etc/apt/apt.conf.d/99-ubuntu-virt.conf) leave the hook config
            # behind pointing at a now-missing binary, breaking every
            # subsequent apt transaction with exit-127.
            GLib.idle_add(self.status_label.set_label, _("Removing leftover dependencies..."))
            GLib.idle_add(self.progress_bar.set_fraction, 0.9)
            subprocess.run(
                ["apt-get", "autoremove", "--purge", "-y"],
                capture_output=True, text=True, timeout=300
            )

            GLib.idle_add(self._removal_finished, removed, failed)

        thread = threading.Thread(target=worker, daemon=True)
        thread.start()

    def _removal_finished(self, removed, failed):
        """Called on the main thread when removal is done."""
        self._running = False
        self.progress_bar.set_fraction(1.0)
        self.progress_bar.set_text(_("Done"))

        # Build summary
        lines = []
        if removed:
            lines.append(_("Removed: {items}").format(items=", ".join(removed)))
        if failed:
            lines.append(_("Failed: {items}").format(items=", ".join(failed)))

        self.status_label.set_label("\n".join(lines))

        # Refresh the row states
        for i, (name, pkgs, desc, row, check, _unused) in enumerate(self.check_rows):
            installed = get_package_status(pkgs)
            check.set_active(False)
            check.set_sensitive(installed)
            row.set_activatable(installed)
            if installed:
                row.set_subtitle(desc)
            else:
                row.set_subtitle(_("{desc}  —  removed").format(desc=desc))
            self.check_rows[i] = (name, pkgs, desc, row, check, installed)

        self.remove_btn.set_sensitive(True)

        dialog = Adw.AlertDialog()
        dialog.set_heading(_("Removal Complete"))
        dialog.set_body("\n".join(lines))
        dialog.add_response("ok", _("OK"))
        dialog.present(self)


class LiteCoreApp(Adw.Application):
    """Application class."""

    def __init__(self):
        super().__init__(application_id=APP_ID)

    def do_activate(self):
        win = self.props.active_window
        if not win:
            win = LiteCoreWindow(self)
        win.present()


def main():
    # Re-exec with pkexec if not root
    if os.geteuid() != 0:
        try:
            with open(ENV_FILE, "w") as _ef:
                for _v in ("LANG", "LANGUAGE", "LC_ALL", "LC_MESSAGES", "LC_NUMERIC"):
                    if _v in os.environ:
                        _ef.write(f"{_v}={os.environ[_v]}\n")
        except Exception:
            pass
        try:
            os.execvp("pkexec", ["pkexec", os.path.abspath(__file__)] + sys.argv[1:])
        except Exception as e:
            print(f"Failed to obtain root privileges: {e}", file=sys.stderr)
            sys.exit(1)

    app = LiteCoreApp()
    app.run(sys.argv)


if __name__ == "__main__":
    main()
