#!/usr/bin/env python3
#--------------------------------------------------------------------------------------------------------
# Name: Linux Lite - Lite Software
# Architecture: all
# Authors: Misko_2083, Jerry Bezencon, gerito1, Johnathan "ShaggyTwoDope" Jenkins, Ralphy
# Website: https://www.linuxliteos.com
# Converted to Python/GTK4/libadwaita
#--------------------------------------------------------------------------------------------------------

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

import subprocess
import os
import sys
import threading
from datetime import datetime
from typing import Optional

# Constants
APP_ID = "com.linuxlite.software"
APP_NAME = "Lite Software"
ICON_PATH = "/usr/share/icons/Papirus/24x24/apps/litesoftware.png"
APPICON_PATH = "/usr/share/liteappsicons/litesoftware/appicons/"
INSTALL_ICON = "/usr/share/liteappsicons/litesoftware/ll-install-software_32x32.png"
REMOVE_ICON = "/usr/share/liteappsicons/litesoftware/ll-remove-software_32x32.png"
LOG_FILE = "/var/log/lite-software.log"
TMP_LOG = "/tmp/lite-software.log"


class SoftwareItem(GObject.Object):
    """Data model for a software item."""

    __gtype_name__ = 'SoftwareItem'

    def __init__(self, alias: str, icon: str, name: str, category: str,
                 description: str, packages: str, optional_packages: str = ""):
        super().__init__()
        self._alias = alias
        self._icon = icon
        self._name = name
        self._category = category
        self._description = description
        self._packages = packages
        self._optional_packages = optional_packages
        self._status = "Not Installed"
        self._selected = False

    @GObject.Property(type=str)
    def alias(self) -> str:
        return self._alias

    @GObject.Property(type=str)
    def icon(self) -> str:
        return self._icon

    @GObject.Property(type=str)
    def name(self) -> str:
        return self._name

    @GObject.Property(type=str)
    def category(self) -> str:
        return self._category

    @GObject.Property(type=str)
    def description(self) -> str:
        return self._description

    @GObject.Property(type=str)
    def packages(self) -> str:
        return self._packages

    @GObject.Property(type=str)
    def optional_packages(self) -> str:
        return self._optional_packages

    @GObject.Property(type=str)
    def status(self) -> str:
        return self._status

    @status.setter
    def status(self, value: str):
        self._status = value

    @GObject.Property(type=bool, default=False)
    def selected(self) -> bool:
        return self._selected

    @selected.setter
    def selected(self, value: bool):
        self._selected = value


# Software catalog - sorted A-Z by name
SOFTWARE_CATALOG = [
    ("audacity", f"{APPICON_PATH}/audacity.png", "Audacity", "Multimedia",
     "Software for recording and editing sounds", "audacity", ""),
    ("bleachbit", f"{APPICON_PATH}/bleachbit.png", "BleachBit", "System",
     "System cleaner and privacy tool", "bleachbit", ""),
    ("blender", f"{APPICON_PATH}/blender.png", "Blender", "Graphics",
     "3D modeling and animation software", "blender", ""),
    ("brasero", f"{APPICON_PATH}/brasero.png", "Brasero", "Multimedia",
     "CD/DVD burning tool", "brasero", ""),
    ("calibre", f"{APPICON_PATH}/calibre.png", "Calibre", "Office",
     "E-book library management application", "calibre", ""),
    ("cheese", f"{APPICON_PATH}/cheese.png", "Cheese", "Multimedia",
     "Webcam application", "cheese", ""),
    ("chromium", f"{APPICON_PATH}/chromium.png", "Chromium", "Internet",
     "Open-source web browser", "chromium-browser", ""),
    ("cpux", f"{APPICON_PATH}/cpu-x.png", "CPU-X", "System",
     "System information tool", "cpu-x", ""),
    ("darktable", f"{APPICON_PATH}/darktable.png", "Darktable", "Graphics",
     "Photo workflow and RAW editor", "darktable", ""),
    ("torrent", f"{APPICON_PATH}/deluge.png", "Deluge", "Internet",
     "Deluge Torrent client software", "deluge deluge-common", ""),
    ("dropbox", f"{APPICON_PATH}/dropbox.png", "Dropbox", "Internet",
     "Popular cloud storage application", "dropbox thunar-dropbox-plugin python3-gpg", ""),
    ("evolution", f"{APPICON_PATH}/evolution.png", "Evolution", "Office",
     "Email and calendar application", "evolution", ""),
    ("filezilla", f"{APPICON_PATH}/filezilla.png", "Filezilla", "Internet",
     "Full-featured FTP/SFTP client", "filezilla", ""),
    ("firefox", f"{APPICON_PATH}/firefox.png", "Firefox", "Internet",
     "Popular open source web browser", "firefox", ""),
    ("flameshot", f"{APPICON_PATH}/flameshot.png", "Flameshot", "Accessories",
     "Powerful screenshot tool", "flameshot", ""),
    ("gamespack", f"{APPICON_PATH}/gamespack.png", "Games Pack", "Games",
     "Solitaire, Mahjongg, Chess, Mines, Sudoku and Tetris",
     "aisleriot gnome-chess gnome-mahjongg gnome-mines gnome-sudoku quadrapassel", ""),
    ("geany", f"{APPICON_PATH}/geany.png", "Geany", "Accessories",
     "IDE and text editor for developers", "geany", "geany-plugins"),
    ("geary", f"{APPICON_PATH}/geary.png", "Geary", "Internet",
     "Lightweight email client", "geary", ""),
    ("gnomeauth", f"{APPICON_PATH}/gnome-authenticator.png", "GNOME Authenticator", "Accessories",
     "Two-factor authentication app", "authenticator", ""),
    ("gnomeboxes", f"{APPICON_PATH}/gnome-boxes.png", "GNOME Boxes", "System",
     "Simple virtual machine manager", "gnome-boxes", ""),
    ("gnomemaps", f"{APPICON_PATH}/gnome-maps.png", "GNOME Maps", "Internet",
     "Maps and navigation application", "gnome-maps", ""),
    ("guvcview", f"{APPICON_PATH}/guvcview.png", "Guvcview", "Multimedia",
     "Webcam software for your computer", "guvcview", ""),
    ("handbrake", f"{APPICON_PATH}/handbrake.png", "Handbrake", "Multimedia",
     "Convert video to nearly any format", "handbrake", ""),
    ("hexchat", f"{APPICON_PATH}/hexchat.png", "HexChat", "Internet",
     "IRC chat client", "hexchat", ""),
    ("inkscape", f"{APPICON_PATH}/inkscape.png", "Inkscape", "Graphics",
     "Vector graphics editor", "inkscape", ""),
    ("kazam", f"{APPICON_PATH}/kazam.png", "Kazam", "Multimedia",
     "Simple screen recording tool", "kazam", ""),
    ("kdeconnect", f"{APPICON_PATH}/kdeconnect.png", "KDE Connect", "Accessories",
     "Connect your phone to your computer", "kdeconnect", ""),
    ("kdenlive", f"{APPICON_PATH}/kdenlive.png", "Kdenlive", "Multimedia",
     "Professional video editor", "kdenlive", ""),
    ("passmgr", f"{APPICON_PATH}/keepassxc.png", "KeePassXC", "Accessories",
     "Full featured password manager", "keepassxc", ""),
    ("kodi", f"{APPICON_PATH}/kodi.png", "Kodi", "Multimedia",
     "The Kodi Media Center", "kodi", ""),
    ("msedge", f"{APPICON_PATH}/microsoft-edge.png", "Microsoft Edge", "Internet",
     "Cross-platform web browser", "microsoft-edge-stable", ""),
    ("mpv", f"{APPICON_PATH}/mpv.png", "MPV", "Multimedia",
     "Lightweight media player", "mpv", ""),
    ("clementine", f"{APPICON_PATH}/clementine.png", "Music Player", "Multimedia",
     "Clementine music player and library organizer", "clementine", ""),
    ("notepadqq", f"{APPICON_PATH}/notepadqq.png", "Notepadqq", "Accessories",
     "Notepad++ like text editor", "notepadqq", ""),
    ("zim", f"{APPICON_PATH}/zim.png", "Note Taking Journal", "Accessories",
     "Zim Note taking/Wiki editing application", "zim", ""),
    ("obs", f"{APPICON_PATH}/obs.png", "OBS Studio", "Multimedia",
     "Record and stream desktop content", "obs-studio", ""),
    ("peek", f"{APPICON_PATH}/peek.png", "Peek", "Multimedia",
     "Animated GIF screen recorder", "peek", ""),
    ("imessenger", f"{APPICON_PATH}/pidgin.png", "Pidgin", "Internet",
     "Multi-protocol Instant Messaging client", "pidgin", ""),
    ("planner", f"{APPICON_PATH}/planner.png", "Planner", "Office",
     "Project management tool", "planner", ""),
    ("playonlinux", f"{APPICON_PATH}/playonlinux.png", "PlayOnLinux", "Cross Platform",
     "Run Windows apps and games on Linux", "playonlinux", ""),
    ("qbittorrent", f"{APPICON_PATH}/qbittorrent.png", "qBittorrent", "Internet",
     "Feature-rich BitTorrent client", "qbittorrent", ""),
    ("redshift", f"{APPICON_PATH}/redshift.png", "Redshift", "Accessories",
     "Adjusts screen color temperature", "redshift redshift-gtk", ""),
    ("remote", f"{APPICON_PATH}/remmina.png", "Remmina", "Internet",
     "Remote desktop client", "remmina", ""),
    ("extras", f"{APPICON_PATH}/extras.png", "Restricted Extras", "Multimedia",
     "Additional codecs and file formats", "ubuntu-restricted-extras", "libavcodec-extra60"),
    ("rhythmbox", f"{APPICON_PATH}/rhythmbox.png", "Rhythmbox", "Multimedia",
     "Music player and organizer", "rhythmbox", ""),
    ("scribus", f"{APPICON_PATH}/scribus.png", "Scribus", "Office",
     "Desktop publishing application", "scribus", ""),
    ("videoedit", f"{APPICON_PATH}/shotcut.png", "Shotcut", "Multimedia",
     "Simple yet powerful video editor", "shotcut", ""),
    ("shutter", f"{APPICON_PATH}/shutter.png", "Shutter", "Accessories",
     "Feature-rich screenshot tool", "shutter", ""),
    ("ssr", f"{APPICON_PATH}/ssr.png", "Simple Screen Recorder", "Multimedia",
     "Simple screen and audio recorder", "simplescreenrecorder", ""),
    ("soundjuicer", f"{APPICON_PATH}/sound-juicer.png", "Sound Juicer", "Multimedia",
     "Extract music tracks from CDs", "sound-juicer", "ubuntu-restricted-extras"),
    ("spotify", f"{APPICON_PATH}/spotify.png", "Spotify", "Multimedia",
     "Digital music service", "spotify-client", ""),
    ("stacer", f"{APPICON_PATH}/stacer.png", "Stacer", "System",
     "System optimizer and monitor", "stacer", ""),
    ("steam", f"{APPICON_PATH}/steam.png", "Steam", "Games",
     "Cross platform gaming client", "steam-installer steam-devices steam:i386", ""),
    ("teamviewer", f"{APPICON_PATH}/teamviewer.png", "Teamviewer", "Internet",
     "Remote Desktop Support software", "teamviewer", ""),
    ("tor", f"{APPICON_PATH}/tor.png", "Tor Web Browser", "Internet",
     "Privacy respecting web browser", "tor-web-browser", ""),
    ("transmission", f"{APPICON_PATH}/transmission.png", "Transmission", "Internet",
     "Lightweight BitTorrent client", "transmission-gtk", ""),
    ("uget", f"{APPICON_PATH}/uget.png", "uGet", "Internet",
     "Download manager", "uget", ""),
    ("vbox", f"{APPICON_PATH}/virtualbox.png", "VirtualBox", "System",
     "Run other operating systems within Linux",
     "virtualbox-qt virtualbox-guest-additions-iso virtualbox-guest-utils virtualbox-guest-x11 virtualbox-dkms", ""),
    ("weather", f"{APPICON_PATH}/weather.png", "Weather Monitor", "System Tray",
     "Weather Monitor Plugin for your tray", "xfce4-weather-plugin", ""),
    ("wine", f"{APPICON_PATH}/wine.png", "Wine", "Cross Platform",
     "Run Windows programs and games on Linux",
     "wine-stable wine32-preloader wine64-preloader wine64-tools winetricks wine-menu-linuxlite", ""),
    ("winff", f"{APPICON_PATH}/winff.png", "WinFF", "Multimedia",
     "Video converter GUI for FFmpeg", "winff", ""),
    ("ytdlp", f"{APPICON_PATH}/yt-dlp.png", "yt-dlp", "Internet",
     "Video downloader from YouTube and more", "yt-dlp", ""),
    ("zoom", f"{APPICON_PATH}/zoom.png", "Zoom", "Internet",
     "Video and audio conferencing application", "zoom", ""),
]


def log_message(message: str):
    """Write a log entry to the log file."""
    try:
        timestamp = datetime.now().strftime("[%D %H:%M:%S]")
        with open(LOG_FILE, "a") as f:
            f.write(f"{timestamp} {message}\n")
    except PermissionError:
        pass


def check_internet() -> bool:
    """Check if internet connection is available."""
    try:
        result = subprocess.run(
            ["curl", "-sk", "google.com"],
            capture_output=True,
            timeout=10
        )
        return result.returncode == 0
    except (subprocess.TimeoutExpired, FileNotFoundError):
        return False


def get_package_status(packages: str) -> str:
    """Check if the main package is installed using dpkg-query."""
    pkg_list = packages.split()
    if not pkg_list:
        return "Not Installed"

    # Use the first (main) package as the indicator
    pkg_name = pkg_list[0].split(":")[0]  # Remove architecture suffix

    try:
        result = subprocess.run(
            ["dpkg-query", "-W", "-f=${Status}", pkg_name],
            capture_output=True,
            text=True
        )
        # Package is installed if status contains "install ok installed"
        if result.returncode == 0 and "install ok installed" in result.stdout:
            return "Installed"
    except FileNotFoundError:
        pass

    return "Not Installed"


def kill_package_managers():
    """Kill any running package managers."""
    for proc in ["synaptic", "gdebi-gtk"]:
        subprocess.run(["pkill", "-9", proc], capture_output=True)


class SoftwareListRow(Gtk.Box):
    """Custom row widget for the software list."""

    def __init__(self, item: SoftwareItem, show_checkbox: bool = True):
        super().__init__(orientation=Gtk.Orientation.HORIZONTAL, spacing=12)
        self.item = item
        self.set_margin_top(8)
        self.set_margin_bottom(8)
        self.set_margin_start(12)
        self.set_margin_end(12)

        # Checkbox
        if show_checkbox:
            self.checkbox = Gtk.CheckButton()
            self.checkbox.set_active(item.selected)
            self.checkbox.connect("toggled", self._on_checkbox_toggled)
            self.append(self.checkbox)

        # Icon
        if os.path.exists(item.icon):
            try:
                pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(item.icon, 32, 32, True)
                icon = Gtk.Image.new_from_pixbuf(pixbuf)
            except GLib.Error:
                icon = Gtk.Image.new_from_icon_name("application-x-executable")
        else:
            icon = Gtk.Image.new_from_icon_name("application-x-executable")
        icon.set_pixel_size(32)
        self.append(icon)

        # Info box
        info_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4)
        info_box.set_hexpand(True)

        # Name and category row
        name_row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
        name_label = Gtk.Label(label=item.name)
        name_label.set_halign(Gtk.Align.START)
        name_label.add_css_class("heading")
        name_row.append(name_label)

        category_label = Gtk.Label(label=item.category)
        category_label.add_css_class("dim-label")
        category_label.add_css_class("caption")
        name_row.append(category_label)

        info_box.append(name_row)

        # Description
        desc_label = Gtk.Label(label=item.description)
        desc_label.set_halign(Gtk.Align.START)
        desc_label.add_css_class("dim-label")
        desc_label.set_wrap(True)
        desc_label.set_xalign(0)
        info_box.append(desc_label)

        self.append(info_box)

        # Status badge
        status_label = Gtk.Label(label=item.status)
        if item.status == "Installed":
            status_label.add_css_class("success")
        else:
            status_label.add_css_class("dim-label")
        status_label.set_valign(Gtk.Align.CENTER)
        self.append(status_label)

    def _on_checkbox_toggled(self, checkbox):
        self.item.selected = checkbox.get_active()


class ProgressDialog(Adw.Window):
    """Progress dialog for package operations."""

    def __init__(self, parent, title: str, text: str):
        super().__init__()
        self.set_title(title)
        self.set_modal(True)
        self.set_transient_for(parent)
        self.set_default_size(500, 150)
        self.set_deletable(False)

        box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=16)
        box.set_margin_top(24)
        box.set_margin_bottom(24)
        box.set_margin_start(24)
        box.set_margin_end(24)

        self.status_label = Gtk.Label(label=text)
        self.status_label.set_wrap(True)
        box.append(self.status_label)

        self.progress_bar = Gtk.ProgressBar()
        self.progress_bar.set_show_text(True)
        box.append(self.progress_bar)

        self.detail_label = Gtk.Label()
        self.detail_label.add_css_class("dim-label")
        self.detail_label.add_css_class("caption")
        self.detail_label.set_wrap(True)
        box.append(self.detail_label)

        self.set_content(box)

    def set_progress(self, fraction: float, text: str = ""):
        self.progress_bar.set_fraction(fraction)
        if text:
            self.progress_bar.set_text(text)

    def set_status(self, text: str):
        self.status_label.set_label(text)

    def set_detail(self, text: str):
        self.detail_label.set_label(text)

    def pulse(self):
        self.progress_bar.pulse()


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

    def __init__(self, app):
        super().__init__(application=app)
        self.set_title(APP_NAME)
        self.set_default_size(900, 700)

        # Hide system window decorations (use CSD only)
        self.set_decorated(False)

        # Load software items
        self.software_items = []
        for data in SOFTWARE_CATALOG:
            item = SoftwareItem(*data)
            self.software_items.append(item)

        # Navigation view for page switching (contains its own headers)
        self.navigation_view = Adw.NavigationView()
        self.navigation_view.set_vexpand(True)

        # Create main menu page
        self.create_main_menu()

        self.set_content(self.navigation_view)

        # Check for root and prepare
        GLib.idle_add(self._initial_setup)

    def _initial_setup(self):
        """Run initial setup after window is shown."""
        if os.geteuid() != 0:
            dialog = Adw.AlertDialog()
            dialog.set_heading("Root Privileges Required")
            dialog.set_body("This application requires root privileges to install and remove software.")
            dialog.add_response("quit", "Quit")
            dialog.set_default_response("quit")
            dialog.connect("response", lambda d, r: self.get_application().quit())
            dialog.present(self)
            return False

        kill_package_managers()
        self._check_internet_and_update()
        return False

    def _check_internet_and_update(self):
        """Check internet and update sources."""
        if not check_internet():
            dialog = Adw.AlertDialog()
            dialog.set_heading("No Internet Access")
            dialog.set_body("Your computer does not seem to be connected to the Internet.\n\n"
                          "Please connect to the Internet before running Lite Software.")
            dialog.add_response("ok", "OK")
            dialog.connect("response", lambda d, r: self.get_application().quit())
            dialog.present(self)
            return

        # Automatically update sources on launch
        self._update_sources()

    def _update_sources(self):
        """Update apt sources."""
        progress = ProgressDialog(self, "Updating Software Sources", "Updating package lists...")
        progress.present()

        def update_thread():
            try:
                process = subprocess.Popen(
                    ["apt-get", "update"],
                    stdout=subprocess.PIPE,
                    stderr=subprocess.STDOUT,
                    text=True
                )

                for line in process.stdout:
                    GLib.idle_add(progress.pulse)
                    if line.strip():
                        GLib.idle_add(progress.set_detail, line.strip()[:80])

                process.wait()

                GLib.idle_add(progress.close)

                if process.returncode != 0:
                    log_message("ERROR: Updating sources has failed.")
                    GLib.idle_add(self._show_error, "Update Failed",
                                 "Updating sources has failed.\n\nRefer to /var/log/lite-software.log for more information.")
                else:
                    log_message("INFO: Software sources were updated.")
            except Exception as e:
                GLib.idle_add(progress.close)
                GLib.idle_add(self._show_error, "Update Failed", str(e))

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

    def _show_error(self, title: str, message: str):
        """Show an error dialog."""
        dialog = Adw.AlertDialog()
        dialog.set_heading(title)
        dialog.set_body(message)
        dialog.add_response("ok", "OK")
        dialog.present(self)

    def _show_info(self, title: str, message: str):
        """Show an info dialog."""
        dialog = Adw.AlertDialog()
        dialog.set_heading(title)
        dialog.set_body(message)
        dialog.add_response("ok", "OK")
        dialog.present(self)

    def create_main_menu(self):
        """Create the main task selector menu."""
        page = Adw.NavigationPage()
        page.set_title(APP_NAME)

        toolbar_view = Adw.ToolbarView()
        header = Adw.HeaderBar()
        toolbar_view.add_top_bar(header)

        # Main content
        content_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=24)
        content_box.set_margin_top(48)
        content_box.set_margin_bottom(48)
        content_box.set_margin_start(48)
        content_box.set_margin_end(48)
        content_box.set_valign(Gtk.Align.CENTER)
        content_box.set_halign(Gtk.Align.CENTER)

        # Title
        title_label = Gtk.Label(label="Please select a Task below")
        title_label.add_css_class("title-2")
        content_box.append(title_label)

        # Buttons box
        buttons_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12)
        buttons_box.set_halign(Gtk.Align.CENTER)

        # Install button
        install_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12)
        install_box.set_halign(Gtk.Align.START)

        if os.path.exists(INSTALL_ICON):
            try:
                pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(INSTALL_ICON, 32, 32, True)
                install_icon = Gtk.Image.new_from_pixbuf(pixbuf)
            except GLib.Error:
                install_icon = Gtk.Image.new_from_icon_name("list-add-symbolic")
        else:
            install_icon = Gtk.Image.new_from_icon_name("list-add-symbolic")
        install_icon.set_pixel_size(32)

        install_btn = Gtk.Button()
        install_btn.set_size_request(280, 60)
        install_btn_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12)
        install_btn_box.set_halign(Gtk.Align.CENTER)
        install_btn_box.append(install_icon)
        install_btn_box.append(Gtk.Label(label="Install Software"))
        install_btn.set_child(install_btn_box)
        install_btn.add_css_class("suggested-action")
        install_btn.add_css_class("pill")
        install_btn.connect("clicked", self._on_install_clicked)
        buttons_box.append(install_btn)

        # Remove button
        if os.path.exists(REMOVE_ICON):
            try:
                pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(REMOVE_ICON, 32, 32, True)
                remove_icon = Gtk.Image.new_from_pixbuf(pixbuf)
            except GLib.Error:
                remove_icon = Gtk.Image.new_from_icon_name("list-remove-symbolic")
        else:
            remove_icon = Gtk.Image.new_from_icon_name("list-remove-symbolic")
        remove_icon.set_pixel_size(32)

        remove_btn = Gtk.Button()
        remove_btn.set_size_request(280, 60)
        remove_btn_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12)
        remove_btn_box.set_halign(Gtk.Align.CENTER)
        remove_btn_box.append(remove_icon)
        remove_btn_box.append(Gtk.Label(label="Remove Software"))
        remove_btn.set_child(remove_btn_box)
        remove_btn.add_css_class("destructive-action")
        remove_btn.add_css_class("pill")
        remove_btn.connect("clicked", self._on_remove_clicked)
        buttons_box.append(remove_btn)

        content_box.append(buttons_box)

        # Instructions
        instructions = Gtk.Label()
        instructions.set_markup(
            "<span size='small'>Select <b>Install Software</b> to add new applications\n"
            "or <b>Remove Software</b> to uninstall existing ones.</span>"
        )
        instructions.add_css_class("dim-label")
        instructions.set_justify(Gtk.Justification.CENTER)
        content_box.append(instructions)

        toolbar_view.set_content(content_box)
        page.set_child(toolbar_view)

        self.navigation_view.add(page)

    def _refresh_package_status(self):
        """Refresh the installation status of all packages."""
        for item in self.software_items:
            item.status = get_package_status(item.packages)

    def _on_install_clicked(self, button):
        """Show install software page."""
        self._refresh_package_status()
        self._show_software_list("install")

    def _on_remove_clicked(self, button):
        """Show remove software page."""
        self._refresh_package_status()
        self._show_software_list("remove")

    def _show_software_list(self, mode: str):
        """Show the software list page."""
        is_install = mode == "install"
        title = "Install Software" if is_install else "Remove Software"
        self.current_filter_status = "Not Installed" if is_install else "Installed"

        page = Adw.NavigationPage()
        page.set_title(title)

        toolbar_view = Adw.ToolbarView()

        # Header bar with action button
        header = Adw.HeaderBar()

        action_btn = Gtk.Button(label="Install" if is_install else "Remove")
        if is_install:
            action_btn.add_css_class("suggested-action")
        else:
            action_btn.add_css_class("destructive-action")
        action_btn.connect("clicked", lambda b: self._on_action_clicked(is_install))
        header.pack_end(action_btn)

        # Select all button
        select_all_btn = Gtk.Button(label="Select All")
        select_all_btn.connect("clicked", lambda b: self._select_all(self.current_filter_status))
        header.pack_start(select_all_btn)

        toolbar_view.add_top_bar(header)

        # Main content box
        main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)

        # Search box
        search_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
        search_box.set_margin_top(12)
        search_box.set_margin_start(12)
        search_box.set_margin_end(12)

        self.search_entry = Gtk.SearchEntry()
        self.search_entry.set_placeholder_text("Search software...")
        self.search_entry.set_hexpand(True)
        self.search_entry.connect("search-changed", self._on_search_changed)
        search_box.append(self.search_entry)

        main_box.append(search_box)

        # Content
        scrolled = Gtk.ScrolledWindow()
        scrolled.set_vexpand(True)

        self.list_box = Gtk.ListBox()
        self.list_box.set_selection_mode(Gtk.SelectionMode.NONE)
        self.list_box.add_css_class("boxed-list")
        self.list_box.set_margin_top(12)
        self.list_box.set_margin_bottom(12)
        self.list_box.set_margin_start(12)
        self.list_box.set_margin_end(12)

        # Clear selections and populate list
        for item in self.software_items:
            item.selected = False
            if item.status == self.current_filter_status:
                row = SoftwareListRow(item)
                row.set_name(item.name.lower())  # Set name for filtering
                self.list_box.append(row)

        scrolled.set_child(self.list_box)
        main_box.append(scrolled)

        toolbar_view.set_content(main_box)
        page.set_child(toolbar_view)

        self.navigation_view.push(page)

    def _on_search_changed(self, search_entry):
        """Filter the software list based on search text."""
        search_text = search_entry.get_text().lower()

        # Iterate through all rows in the list box
        row = self.list_box.get_first_child()
        while row:
            if isinstance(row, Gtk.ListBoxRow):
                child = row.get_child()
                if isinstance(child, SoftwareListRow):
                    item = child.item
                    # Check if search text matches name, category, or description
                    visible = (search_text in item.name.lower() or
                              search_text in item.category.lower() or
                              search_text in item.description.lower())
                    row.set_visible(visible)
            row = row.get_next_sibling()

    def _select_all(self, filter_status: str):
        """Select all items with given status."""
        for item in self.software_items:
            if item.status == filter_status:
                item.selected = True
        # Refresh the current page
        current_page = self.navigation_view.get_visible_page()
        if current_page:
            # Find all checkboxes and set them
            self._update_checkboxes_in_widget(current_page.get_child(), True)

    def _update_checkboxes_in_widget(self, widget, state: bool):
        """Recursively find and update all checkboxes."""
        if isinstance(widget, Gtk.CheckButton):
            widget.set_active(state)
        elif hasattr(widget, 'get_first_child'):
            child = widget.get_first_child()
            while child:
                self._update_checkboxes_in_widget(child, state)
                child = child.get_next_sibling()
        elif hasattr(widget, 'get_child'):
            child = widget.get_child()
            if child:
                self._update_checkboxes_in_widget(child, state)

    def _on_action_clicked(self, is_install: bool):
        """Handle install/remove button click."""
        selected_items = [item for item in self.software_items if item.selected]

        if not selected_items:
            self._show_info(
                "No Selection",
                f"No application was selected for {'installation' if is_install else 'removal'}."
            )
            return

        # Build confirmation message
        names = "\n".join([f"• {item.name}" for item in selected_items])
        action = "installation" if is_install else "removal"

        dialog = Adw.AlertDialog()
        dialog.set_heading(f"Confirm {'Installation' if is_install else 'Removal'}")
        dialog.set_body(f"The following software has been selected:\n\n{names}\n\nDo you want to proceed with the {action}?")
        dialog.add_response("cancel", "Cancel")
        dialog.add_response("confirm", "Install" if is_install else "Remove")

        if is_install:
            dialog.set_response_appearance("confirm", Adw.ResponseAppearance.SUGGESTED)
        else:
            dialog.set_response_appearance("confirm", Adw.ResponseAppearance.DESTRUCTIVE)

        dialog.connect("response", lambda d, r: self._execute_action(selected_items, is_install) if r == "confirm" else None)
        dialog.present(self)

    def _execute_action(self, items: list, is_install: bool):
        """Execute install or remove action."""
        # Collect all packages
        packages = []
        for item in items:
            packages.extend(item.packages.split())
            if item.optional_packages:
                packages.extend(item.optional_packages.split())

        action = "install" if is_install else "remove"
        log_message(f"INFO: {'Installation' if is_install else 'Removal'} of packages initiated: {' '.join(packages)}")

        progress = ProgressDialog(
            self,
            f"{'Installing' if is_install else 'Removing'} Software",
            f"{'Installing' if is_install else 'Removing'} packages...\nThis may take a while."
        )
        progress.present()

        def action_thread():
            try:
                cmd = ["apt-get", action, "-f", "-y"] + packages

                with open(TMP_LOG, "w") as log_file:
                    process = subprocess.Popen(
                        cmd,
                        stdout=subprocess.PIPE,
                        stderr=subprocess.STDOUT,
                        text=True,
                        env={**os.environ, "DEBIAN_FRONTEND": "noninteractive"}
                    )

                    for line in process.stdout:
                        log_file.write(line)
                        log_file.flush()
                        GLib.idle_add(progress.pulse)

                        # Parse progress indicators
                        line_stripped = line.strip()
                        if line_stripped:
                            # Show relevant status updates
                            if any(x in line_stripped for x in ["Get:", "Unpacking", "Setting up", "Removing", "Processing"]):
                                GLib.idle_add(progress.set_detail, line_stripped[:100])

                    process.wait()

                GLib.idle_add(progress.close)

                if process.returncode != 0:
                    # Get error message from log
                    try:
                        with open(TMP_LOG, "r") as f:
                            for line in f:
                                if line.startswith("E:"):
                                    err_msg = line[2:].strip()
                                    break
                            else:
                                err_msg = "Unknown error occurred"
                    except:
                        err_msg = "Unknown error occurred"

                    log_message(f"ERROR: {err_msg}")
                    GLib.idle_add(self._show_error, f"{'Installation' if is_install else 'Removal'} Failed",
                                 f"{APP_NAME} Error:\n\n{err_msg}\n\nMake sure your computer is connected to the Internet and try again.")
                else:
                    log_message(f"INFO: {'Installation' if is_install else 'Removal'} was a success.")
                    GLib.idle_add(self._show_info, f"{'Installation' if is_install else 'Removal'} Complete",
                                 f"{'Installation' if is_install else 'Removal'} successfully completed.")
                    GLib.idle_add(self._go_back_to_main)

            except Exception as e:
                GLib.idle_add(progress.close)
                GLib.idle_add(self._show_error, "Error", str(e))

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

    def _go_back_to_main(self):
        """Navigate back to main menu."""
        self.navigation_view.pop()


class LiteSoftwareApp(Adw.Application):
    """Main application class."""

    def __init__(self):
        super().__init__(
            application_id=APP_ID,
            flags=Gio.ApplicationFlags.FLAGS_NONE
        )

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


def main():
    # Check if running as root, if not, use pkexec to elevate
    if os.geteuid() != 0:
        try:
            # Allow root to access X display
            display = os.environ.get('DISPLAY', ':0')
            subprocess.run(['xhost', '+si:localuser:root'], capture_output=True)

            # Set up environment variables file for the elevated process
            env_file = '/tmp/.lite-software-env'
            with open(env_file, 'w') as f:
                for var in ['DISPLAY', 'XAUTHORITY', 'WAYLAND_DISPLAY', 'XDG_RUNTIME_DIR']:
                    if var in os.environ:
                        f.write(f'{var}={os.environ[var]}\n')

            # Use pkexec to run the installed script directly (matches policy file)
            os.execvp("pkexec", ["pkexec", "/usr/bin/lite-software"] + sys.argv[1:])
        except Exception as e:
            print(f"Failed to elevate privileges: {e}")
            sys.exit(1)

    # Running as root - load environment from temp file if available
    env_file = '/tmp/.lite-software-env'
    if os.path.exists(env_file):
        try:
            with open(env_file, 'r') as f:
                for line in f:
                    line = line.strip()
                    if '=' in line:
                        key, value = line.split('=', 1)
                        os.environ[key] = value
            os.unlink(env_file)
        except:
            pass

    app = LiteSoftwareApp()
    return app.run(sys.argv)


if __name__ == "__main__":
    sys.exit(main())
