#!/usr/bin/env python3
"""
SolydXK Favorites
Module for handling favorite applications on a Debian-based system.

Author: Arjen Balfoort 
Date: 2026-01-16
"""

import os
import json
import logging
import subprocess
from typing import Optional
import apt

from image import ImageHandler

import gi
gi.require_version("AppStream", "1.0")
gi.require_version('Gtk', '4.0')
from gi.repository import AppStream, Gtk, Gdk, Pango

class AppStreamPackageInfo:
    """
    Represents a single package with all relevant info.
    """
    def __init__(
        self,
        package_id: str,
        package_name: str='',
        name: str='',
        summary: str='',
        icon: str='',
        is_installed: bool=False,
    ):
        self.package_id: str = package_id
        self.package_name: str = package_name
        self.name: str = name
        self.summary: str = summary
        self.icon: str = icon
        self.is_installed: bool = is_installed

    def to_dict(self):
        """Converts the object to a dictionary."""
        return {
            'package_id': self.package_id,
            'package_name': self.package_name,
            'name': self.name,
            'summary': self.summary,
            'icon': self.icon,
            'is_installed': self.is_installed,
        }


class FavoriteApps():
    """
    Class to fill the favorites dictionary with information about Debian packages

    Attributes:
        is_xfce (bool): Whether the system is running Xfce or not
        favorites (dict): Dictionary to store information about Debian packages
    """
    def __init__(self, logging_handler):
        self.script_dir: str = os.path.dirname(os.path.abspath(__file__))
        self.logging: logging = logging_handler
        self.data_dir: str = os.path.join(self.script_dir, "data")
        self.images_dir: str = os.path.join(self.script_dir, "images")

        # Image handler
        self.image_handler: ImageHandler = ImageHandler()

        self.apt: apt.Cache = apt.Cache()
        self.pool: AppStream.Pool = AppStream.Pool()
        self.pool.load()
        self.components: list[AppStream.Component] = self.pool.get_components().as_array()

        # Get installed packages
        self.installed_deb_pacakges: list[str] = self._get_debian_package_names()

        # Get current desktop (kde or xfce)
        self.desktop: str = os.getenv('XDG_CURRENT_DESKTOP').lower()

        # Get current icon theme
        display: Gdk.Display = Gdk.Display.get_default()
        self.icon_theme: Gtk.IconTheme = Gtk.IconTheme.get_for_display(display)

        self.favorites: list[AppStreamPackageInfo] = []
        with open(os.path.join(self.data_dir, "favorites.json"), "r", encoding="utf-8") as f:
            for key, packages in json.load(f).items():
                if key in ['all', self.desktop]:
                    for package in packages:
                        info = self.get_package_info(package)
                        if info:
                            self.favorites.append(info)

    def get_package_info(self, pkgname: str) -> Optional[AppStreamPackageInfo]:
        """
        Get package information
        
        Args:
            pkgname (str): A package name to get summary details for.
        
        Returns:
            dict: AppStreamPackageInfo objects or None.
        """
        for component in self.components:
            if component.get_pkgname() == pkgname or component.get_id() == pkgname:
                pkg_info = self.get_summary_info(component)
                return pkg_info

        pkg_info = self._get_apt_summary_info(pkgname)
        if pkg_info:
            return pkg_info

        return None

    def sort_apps(self,
                  pkg_info: list[AppStreamPackageInfo],
                  sort_by: str = 'name',
                  reverse: bool = False) -> list[AppStreamPackageInfo]:
        """
        Sorts a list of AppStreamPackageInfo objects by the specified attribute.

        Args:
            pkg_info: List of AppStreamPackageInfo objects to sort.
            sort_by: Attribute to sort by (e.g., 'package_name', 'name'). Defaults to 'name'.
            reverse: If True, sorts in descending order. Defaults to False.

        Returns:
            Sorted list of AppStreamPackageInfo objects.
        """
        try:
            return sorted(
                pkg_info,
                key=lambda obj: getattr(obj, sort_by).lower(),
                reverse=reverse
            )
        except AttributeError:
            self.logging.error(f"Invalid sort attribute: {sort_by}")
            return pkg_info

    def get_summary_info(self, component: AppStream.Component) -> AppStreamPackageInfo:
        """ Builds a package info object. """
        pkg_info = AppStreamPackageInfo(package_id=component.get_id(),
                                        package_name=component.get_pkgname(),
                                        name=component.get_name(),
                                        summary=component.get_summary(),
                                        is_installed=False)
        pkg_info.is_installed = pkg_info.package_name in self.installed_deb_pacakges
        pkg_info.icon = self._get_icon(component)
        return pkg_info

    def get_gtk_box_summary(self, pkg_info: AppStreamPackageInfo) -> Gtk.Box:
        """
        Create a summary GTK box for a package.
        
        Args:
            pkg_info (AppStreamPackageInfo): A package info object.

        CSS Classes:
            .summary_box
            .summary_box picture
            .summary_box label
            .summary_box label.name
            .summary_box label.summary
            .summary_box checkbutton
        
        Returns:
            tuple[str, Gtk.Box]: pkg_id, A GTK box for the package summary.
        """
        box = Gtk.Box()
        box.set_orientation(Gtk.Orientation.HORIZONTAL)
        box.set_spacing(12)
        box.set_css_classes(['summary_box'])

        # Create a hand cursor
        hand_cursor = Gdk.Cursor.new_from_name("pointer", None)
        box.set_cursor(hand_cursor)

        # Add icon
        icon = Gtk.Picture()
        self.image_handler.set_picture(icon, pkg_info.icon, 48)
        icon.set_content_fit(Gtk.ContentFit.SCALE_DOWN)
        icon.set_valign(Gtk.Align.CENTER)
        box.append(icon)

        # Add name and summary to the label_box
        label_box = Gtk.Box()
        label_box.set_orientation(Gtk.Orientation.VERTICAL)
        label_box.set_hexpand(True)

        # Add name
        label_name = Gtk.Label(label=pkg_info.name)
        label_name.set_halign(Gtk.Align.START)
        context = label_name.get_style_context()
        context.add_class("name")
        label_box.append(label_name)

        # Add summary
        label_summary = Gtk.Label(label=pkg_info.summary)
        # Limit the text length to the width of the label
        label_summary.set_ellipsize(Pango.EllipsizeMode.END)
        label_summary.set_halign(Gtk.Align.START)
        context = label_summary.get_style_context()
        context.add_class("summary")
        label_box.append(label_summary)

        box.append(label_box)

        if pkg_info.is_installed:
            installed_icon = Gtk.Picture().new_for_filename(
                os.path.join(self.images_dir, "installed.svg")
            )
            installed_icon.set_content_fit(Gtk.ContentFit.SCALE_DOWN)
            installed_icon.set_valign(Gtk.Align.END)
            box.append(installed_icon)
        else:
            install_checkbox = Gtk.CheckButton()
            install_checkbox.set_can_target(False)  # Disable default toggle
            install_checkbox.set_can_focus(False)
            install_checkbox.package_id = pkg_info.package_id
            install_checkbox.package_name = pkg_info.package_name
            install_checkbox.set_valign(Gtk.Align.END)
            box.append(install_checkbox)

        return box

    # =========================================
    # Private methods
    # =========================================

    @staticmethod
    def _get_debian_package_names() -> list[str]:
        """ Returns a list of installed Debian package names. """
        result = subprocess.run(
            ["dpkg-query", "--show", "-f=${Package}\n"],
            capture_output=True,
            text=True,
            check=True
        )
        return result.stdout.splitlines()

    def _get_apt_summary_info(self, pkgname: str) -> Optional[AppStreamPackageInfo]:
        """
        Get summary info from apt.
        
        Args:
            pkgname (str): The package name to get info for.
        
        Returns:
            AppStreamPackageInfo object.
        """
        if not pkgname:
            return {}

        # Only get the package name
        pkgname = pkgname.split(':', 1)[0]
        info = AppStreamPackageInfo(package_id=pkgname,
                                    package_name=pkgname,
                                    name=pkgname.capitalize())

        try:
            apt_pkg = self.apt[pkgname].installed
            apt_pkg_candidate = self.apt[pkgname].candidate
            if apt_pkg is None:
                # Not installed
                apt_pkg = apt_pkg_candidate

            info.summary = apt_pkg.summary
            info.is_installed = apt_pkg.is_installed
            if self.icon_theme.has_icon(pkgname):
                info.icon = pkgname
            else:
                info.icon = self._get_default_icon(None)
            return info

        except Exception as e:
            self.logging.error(f"Error getting apt info for {pkgname}: {e}")
            return None

    def _get_icon(self, component: AppStream.Component) -> str:
        """ Gets the icon for a component. """
        icon_paths = []
        icon_names = []
        icon_nodes = component.get_icons()
        for icon_node in icon_nodes:
            icon_path = icon_node.get_filename()
            icon_name = icon_node.get_name()
            if icon_path is not None:
                icon_paths.append(icon_path)
                continue
            if icon_name is not None and '.' not in icon_name:
                icon_names.append(icon_name)

        # Try to get the icon from the icon path
        for icon_path in reversed(icon_paths):
            if os.path.exists(icon_path):
                return icon_path

        # Try to get the icon from the icon name
        for icon_name in reversed(icon_names):
            if self.icon_theme.has_icon(icon_name):
                return icon_name

        # Try to get the icon from the package id
        icon = component.get_id().lower()
        if self.icon_theme.has_icon(icon):
            return icon

        # Try to get the icon from parts of the id
        for icon_part in icon.split('.'):
            if self.icon_theme.has_icon(icon_part):
                return icon_part

        # Use default icon
        return self._get_default_icon(component)

    def _get_default_icon(self, component: AppStream.Component=None) -> str:
        """ No icon: use default icons per component kind """
        icon = os.path.join(self.images_dir, "default.svg")
        if component is None:
            return icon

        if component.get_kind() == AppStream.ComponentKind.CONSOLE_APP:
            icon = os.path.join(self.images_dir, "console.svg")
        elif component.get_kind() == AppStream.ComponentKind.WEB_APP:
            icon = os.path.join(self.images_dir, "web.svg")
        elif component.get_kind() == AppStream.ComponentKind.DESKTOP_APP:
            icon = os.path.join(self.images_dir, "desktop.svg")

        return icon
