#!/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

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 = {
    "Evince": ["evince.desktop", "org.gnome.Evince.desktop"],
    "GNU Info": ["info.desktop"],
    "GNOME Disks": ["org.gnome.DiskUtility.desktop"],
    "GNOME Paint": ["gnome-paint.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-Profile-Browser.desktop",
        "org.gnome.Shotwell-Viewer.desktop",
    ],
    "Simple Scan": ["simple-scan.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"),
    ("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


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(f"{description}  —  not installed")
            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

        names = ", ".join(n for n, _ in selected)
        dialog = Adw.AlertDialog()
        dialog.set_heading("Confirm Removal")
        dialog.set_body(f"The following will be removed:\n\n{names}\n\nThis cannot be undone.")
        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, f"Removing {name}...")
                GLib.idle_add(self.progress_bar.set_fraction, i / total)
                GLib.idle_add(
                    self.progress_bar.set_text,
                    f"{i + 1} of {total}"
                )

                try:
                    result = subprocess.run(
                        ["apt-get", "purge", "-y"] + pkgs.split(),
                        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, _, pkgs, _ 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(f"Removed: {', '.join(removed)}")
        if failed:
            lines.append(f"Failed: {', '.join(failed)}")

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

        # Refresh the row states
        for i, (name, pkgs, desc, row, check, _) 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(f"{desc}  —  removed")
            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:
            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()
