#!/usr/bin/env python3
import gi, sys, subprocess, threading, os, re, shutil, shlex
gi.require_version('Gtk', '4.0')
gi.require_version('Adw', '1')

from gi.repository import Gtk, Adw, Gio, GLib
from dataclasses import dataclass
from typing import List, Optional

# ---------------------------------------------------
# Fester Pfad für externe Skripte
# ---------------------------------------------------
SCRIPT_DIR = "/usr/local/bin/software/scripts"

# ----------------------------- Datenmodell -----------------------------
@dataclass
class App:
    name: str
    description: str
    category: str
    xbps_name: Optional[str] = None
    flatpak_id: Optional[str] = None
    script_path: Optional[str] = None
    icon: str = "application-x-executable"
    needs_root: bool = False  # Skript mit sudo starten?

# ------------------------- Installations-Cache -------------------------
class InstallCache:
    """Holt installierte Pakete/Apps einmalig und cached sie."""
    def __init__(self):
        self.xbps = set()        # paketnamen (ohne version)
        self.flatpaks = set()    # app IDs
        self.ready = False

    def refresh(self):
        self.xbps.clear()
        self.flatpaks.clear()

        # XBPS: installierte Pakete
        try:
            out = subprocess.run(['xbps-query', '-l'], capture_output=True, text=True, timeout=8)
            if out.returncode == 0:
                for line in out.stdout.splitlines():
                    parts = line.strip().split()
                    if len(parts) >= 2:
                        pkg_full = parts[1]
                        m = re.match(r'^(.+?)-\d', pkg_full)
                        base = m.group(1) if m else pkg_full.split('-')[0]
                        if base:
                            self.xbps.add(base.lower())
        except Exception:
            pass

        # Flatpak: installierte App-IDs
        try:
            out = subprocess.run(['flatpak', 'list', '--app', '--columns=application'],
                                 capture_output=True, text=True, timeout=6)
            if out.returncode == 0:
                for appid in out.stdout.splitlines():
                    appid = appid.strip()
                    if appid:
                        self.flatpaks.add(appid)
        except Exception:
            pass

        self.ready = True

# ------------------------------ Anwendung -----------------------------
class VoidSoftwareStore(Adw.Application):
    def __init__(self):
        super().__init__(application_id="com.voidlinux.softwarestore")

        # ✅ System Light/Dark automatisch übernehmen
        self.style_manager = Adw.StyleManager.get_default()
        self.style_manager.set_color_scheme(Adw.ColorScheme.DEFAULT)

        self.main_window = None
        self.apps_data = self._load_apps_data()

    def do_activate(self):
        if not self.main_window:
            self.main_window = MainWindow(application=self, apps_data=self.apps_data)
        self.main_window.present()

    def _load_apps_data(self) -> List[App]:
        """Lädt die Anwendungsdaten"""
        return [
            # Büro
            App("LibreOffice", "Vollständige Office-Suite", "Büro",
                xbps_name="libreoffice libreoffice-i18n-de", flatpak_id="org.libreoffice.LibreOffice",
                icon="libreoffice-startcenter"),
            App("OnlyOffice", "Moderne Office-Suite", "Büro",
                flatpak_id="org.onlyoffice.desktopeditors", icon="onlyoffice-desktopeditors"),
            App("WPS Office", "Microsoft Office-kompatible Suite", "Büro",
                flatpak_id="com.wps.Office", icon="com.wps.Office"),
            App("FreeOffice", "Kostenlose Office-Suite", "Büro",
                script_path=os.path.join(SCRIPT_DIR, "install_freeoffice.sh"), icon="freeoffice", needs_root=True),
            App("Scribus", "Desktop-Publishing", "Büro", xbps_name="scribus", icon="scribus"),
            App("PDF Arranger", "PDF-Seiten bearbeiten", "Büro", xbps_name="pdfarranger", icon="pdfarranger"),
            App("Okular", "Dokumentenbetrachter", "Büro", xbps_name="okular", icon="okular"),
            App("Xournal++", "Notizen und PDF-Annotation", "Büro", xbps_name="xournalpp", icon="com.github.xournalpp.xournalpp"),
            App("GnuCash", "Finanzverwaltung", "Büro", xbps_name="gnucash", icon="gnucash"),
            App("Evince", "PDF-Betrachter", "Büro", xbps_name="evince", icon="org.gnome.Evince"),
            App("XMind 8", "Mindmapping (XMind 8)", "Büro", flatpak_id="net.xmind.XMind", icon="net.xmind.XMind"),

            # Grafik
            App("GIMP", "Bildbearbeitung", "Grafik", xbps_name="gimp", flatpak_id="org.gimp.GIMP", icon="org.gimp.GIMP"),
            App("Inkscape", "Vektorgrafiken", "Grafik", xbps_name="inkscape", flatpak_id="org.inkscape.Inkscape", icon="org.inkscape.Inkscape"),
            App("Blender", "3D-Modellierung & Animation", "Grafik", xbps_name="blender", flatpak_id="org.blender.Blender", icon="blender"),
            App("Krita", "Digitales Malen und Zeichnen", "Grafik", xbps_name="krita", flatpak_id="org.kde.krita", icon="krita"),
            App("Pinta", "Einfache Bildbearbeitung", "Grafik", xbps_name="pinta", flatpak_id="com.github.PintaProject.Pinta", icon="pinta"),
            App("Darktable", "RAW-Foto-Entwicklung", "Grafik", xbps_name="darktable", flatpak_id="org.darktable.Darktable", icon="darktable"),
            App("Flameshot", "Screenshot-Tool", "Grafik", xbps_name="flameshot", flatpak_id="org.flameshot.Flameshot", icon="flameshot"),
            App("Draw.io", "Diagramme und Flowcharts", "Grafik", flatpak_id="com.jgraph.drawio.desktop", icon="drawio"),

            # Multimedia
            App("VLC", "Media Player", "Multimedia", xbps_name="vlc", flatpak_id="org.videolan.VLC", icon="vlc"),
            App("Audacity", "Audio-Editor", "Multimedia", xbps_name="audacity", flatpak_id="org.audacityteam.Audacity", icon="audacity"),
            App("OBS Studio", "Streaming & Aufnahme", "Multimedia", xbps_name="obs", flatpak_id="com.obsproject.Studio", icon="com.obsproject.Studio"),
            App("mpv", "Leichter Media Player", "Multimedia", xbps_name="mpv", flatpak_id="io.mpv.Mpv", icon="mpv"),
            App("Clapper", "GNOME Media Player", "Multimedia", xbps_name="clapper", flatpak_id="com.github.rafostar.Clapper", icon="com.github.rafostar.Clapper"),
            App("Celluloid", "GTK+ Media Player", "Multimedia", xbps_name="celluloid", flatpak_id="io.github.celluloid_player.Celluloid", icon="io.github.celluloid_player.Celluloid"),
            App("Lollypop", "GNOME Musik-Player", "Multimedia", xbps_name="lollypop", flatpak_id="org.gnome.Lollypop", icon="org.gnome.Lollypop"),
            App("Kdenlive", "Video Editor", "Multimedia", xbps_name="kdenlive", flatpak_id="org.kde.kdenlive", icon="kdenlive"),
            App("Shotcut", "Video Editor", "Multimedia", xbps_name="shotcut", flatpak_id="org.shotcut.Shotcut", icon="shotcut"),
            App("Spotify", "Musik-Streaming", "Multimedia", flatpak_id="com.spotify.Client", icon="spotify"),

            # Internet
            App("Firefox", "Webbrowser", "Internet", xbps_name="firefox", flatpak_id="org.mozilla.firefox", icon="firefox"),
            App("Chromium", "Webbrowser", "Internet", xbps_name="chromium", flatpak_id="org.chromium.Chromium", icon="chromium"),
            App("Discord", "Chat-Anwendung", "Internet", flatpak_id="com.discordapp.Discord", icon="discord"),
            App("Vivaldi", "Browser auf Chromium-Basis", "Internet", xbps_name="vivaldi", flatpak_id="com.vivaldi.Vivaldi", icon="vivaldi"),
            App("Telegram Desktop", "Messenger", "Internet", xbps_name="telegram-desktop", flatpak_id="org.telegram.desktop", icon="telegram"),
            App("Thunderbird", "E-Mail-Client", "Internet", xbps_name="thunderbird thunderbird-i18n-de", flatpak_id="org.mozilla.Thunderbird", icon="thunderbird"),
            App("Google Chrome", "Google Webbrowser", "Internet", flatpak_id="com.google.Chrome", icon="com.google.Chrome"),
            App("Signal Desktop", "Sicherer Messenger", "Internet", flatpak_id="org.signal.Signal", icon="org.signal.Signal"),

            # Gaming
            App("Steam", "Spieleplattform", "Gaming", xbps_name="steam", flatpak_id="com.valvesoftware.Steam", icon="steam"),
            App("Lutris", "Spiele-Manager", "Gaming", xbps_name="lutris", flatpak_id="net.lutris.Lutris", icon="lutris"),
            App("Heroic Games Launcher", "Epic & GOG Launcher", "Gaming", flatpak_id="com.heroicgameslauncher.hgl", icon="com.heroicgameslauncher.hgl"),
            App("RetroArch", "Frontend für Emulatoren", "Gaming", xbps_name="retroarch", flatpak_id="org.libretro.RetroArch", icon="retroarch"),
            App("MangoHud & Goverlay", "Performance-Overlay", "Gaming", xbps_name="mangohud goverlay", icon="goverlay"),
            App("ProtonUp-Qt", "Proton-GE verwalten", "Gaming", flatpak_id="net.davidotek.pupgui2", icon="net.davidotek.pupgui2"),
            App("Bottles", "Windows-Software ausführen", "Gaming", flatpak_id="com.usebottles.bottles", icon="com.usebottles.bottles"),
            App("Prism Launcher", "Minecraft Launcher", "Gaming", flatpak_id="org.prismlauncher.PrismLauncher", icon="org.prismlauncher.PrismLauncher"),
            App("PCSX2", "PlayStation 2 Emulator", "Gaming", flatpak_id="net.pcsx2.PCSX2", icon="net.pcsx2.PCSX2"),
            App("Piper", "Gaming-Maus Konfiguration", "Gaming", xbps_name="piper", icon="piper"),
            App("Mumble", "Voice-Chat", "Gaming", xbps_name="mumble", icon="mumble"),

            # Tools
            App("VS Code", "Code-Editor", "Tools", flatpak_id="com.visualstudio.code", icon="com.visualstudio.code"),
            App("Git", "Versionskontrolle", "Tools", xbps_name="git", icon="git"),
            App("QMapshack", "GPS-Karten-Tool", "Tools",
                script_path=os.path.join(SCRIPT_DIR, "install_qmapshack.sh"), icon="qmapshack", needs_root=True),
            App("FileZilla", "FTP/SFTP-Client", "Tools", xbps_name="filezilla", flatpak_id="org.filezillaproject.Filezilla", icon="filezilla"),
            App("qBittorrent", "BitTorrent-Client", "Tools", xbps_name="qbittorrent", flatpak_id="org.qbittorrent.qBittorrent", icon="qbittorrent"),
            App("TeamViewer", "Remote Support", "Tools", flatpak_id="com.teamviewer.TeamViewer", icon="com.teamviewer.TeamViewer"),

            # System
            App("GParted", "Partitionsverwaltung", "System", xbps_name="gparted", icon="gparted"),
            App("Htop", "Systemmonitor (Terminal)", "System", xbps_name="htop", icon="htop"),
            App("Timeshift", "System-Backups", "System", xbps_name="timeshift", icon="com.teejeetech.Timeshift"),
            App("btop", "Moderner Systemmonitor", "System", xbps_name="btop", icon="utilities-system-monitor"),
            App("BleachBit", "Systembereinigung", "System", xbps_name="bleachbit", icon="bleachbit"),
            App("Deja Dup", "Backup-Tool", "System", xbps_name="deja-dup", icon="deja-dup"),
            App("Multimedia-Codecs", "Installiert Audio/Video-Codecs (Skript)", "System",
                script_path=os.path.join(SCRIPT_DIR, "codecs.sh"), icon="applications-multimedia", needs_root=True),
        ]

# ------------------------------ Hauptfenster ------------------------------
class MainWindow(Adw.ApplicationWindow):
    def __init__(self, application, apps_data):
        super().__init__(application=application)
        self.apps_data = apps_data
        self.current_category = "Alle"
        self.search_term = ""
        self.filtered_apps = apps_data.copy()

        self.set_title("Void Software Store")
        self.set_default_size(1250, 900)
        self.set_size_request(1000, 700)

        self._icon_cache = {}
        self.install_cache = InstallCache()

        # ✅ System Light/Dark automatisch übernehmen
        self.style_manager = Adw.StyleManager.get_default()
        self.style_manager.set_color_scheme(Adw.ColorScheme.DEFAULT)

        self._setup_ui()
        GLib.idle_add(self._filter_apps)

        threading.Thread(target=self._warm_up_cache, daemon=True).start()

    def _warm_up_cache(self):
        self.install_cache.refresh()
        GLib.idle_add(self._update_app_list)

    # ------------------------------ UI-Bau ------------------------------
    def _setup_ui(self):
        main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
        self.set_content(main_box)

        self._header_bar = self._create_header_bar()
        main_box.append(self._header_bar)

        paned = Gtk.Paned(orientation=Gtk.Orientation.HORIZONTAL)
        paned.set_position(280)
        main_box.append(paned)

        left_panel = self._create_left_panel()
        paned.set_start_child(left_panel)

        right_panel = self._create_right_panel()
        paned.set_end_child(right_panel)

    def _create_header_bar(self) -> Adw.HeaderBar:
        header_bar = Adw.HeaderBar()
        self._main_title = Gtk.Label(label="Void Software Store")
        header_bar.set_title_widget(self._main_title)

        self.back_button = Gtk.Button(icon_name="go-previous-symbolic")
        self.back_button.connect("clicked", self._on_back_clicked)
        self.back_button.set_visible(False)
        header_bar.pack_start(self.back_button)

        update_button = Gtk.Button(label="System Update")
        update_button.add_css_class("suggested-action")
        update_button.set_tooltip_text("topgrade starten")
        update_button.connect("clicked", self._on_update_clicked)
        header_bar.pack_end(update_button)
        return header_bar

    def _create_left_panel(self) -> Gtk.Widget:
        box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=15)
        box.set_margin_top(15); box.set_margin_bottom(15)
        box.set_margin_start(15); box.set_margin_end(15)
        box.set_size_request(260, -1)

        search_group = Adw.PreferencesGroup(title="Suche")
        self.search_entry = Gtk.SearchEntry(placeholder_text="Programme suchen...")
        self.search_entry.connect("search-changed", self._on_search_changed)
        search_group.add(self.search_entry)
        box.append(search_group)

        categories_group = Adw.PreferencesGroup(title="Kategorien", margin_top=10)
        self.category_listbox = Gtk.ListBox(selection_mode=Gtk.SelectionMode.SINGLE)
        self.category_listbox.add_css_class("navigation-sidebar")
        categories_group.add(self.category_listbox)

        categories = {
            "Alle": "view-grid-symbolic", "Installiert": "software-installed-symbolic",
            "Büro": "accessories-text-editor-symbolic", "Grafik": "applications-graphics-symbolic",
            "Multimedia": "applications-multimedia-symbolic", "Internet": "applications-internet-symbolic",
            "Gaming": "applications-games-symbolic", "Tools": "applications-development-symbolic",
            "System": "applications-system-symbolic"
        }

        for name, icon_name in categories.items():
            row = Adw.ActionRow(title=name, icon_name=icon_name)
            row.set_name(name)
            self.category_listbox.append(row)

        self.category_listbox.select_row(self.category_listbox.get_row_at_index(0))
        self.category_listbox.connect("row-selected", self._on_category_selected)
        box.append(categories_group)
        return box

    def _create_right_panel(self) -> Gtk.Widget:
        right_container = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)

        self.main_stack = Adw.ViewStack()

        self.main_scroll = Gtk.ScrolledWindow(
            hscrollbar_policy=Gtk.PolicyType.NEVER,
            vscrollbar_policy=Gtk.PolicyType.AUTOMATIC
        )
        self.apps_flowbox = Gtk.FlowBox(
            valign=Gtk.Align.START, max_children_per_line=10, min_children_per_line=3,
            row_spacing=15, column_spacing=15, selection_mode=Gtk.SelectionMode.NONE,
            homogeneous=False, margin_top=20, margin_bottom=20, margin_start=20, margin_end=20
        )
        self.main_scroll.set_child(self.apps_flowbox)
        self.main_stack.add_named(self.main_scroll, "grid")

        self.detail_scroll = Gtk.ScrolledWindow(
            hscrollbar_policy=Gtk.PolicyType.NEVER,
            vscrollbar_policy=Gtk.PolicyType.AUTOMATIC
        )
        self.main_stack.add_named(self.detail_scroll, "details")

        right_container.append(self.main_stack)

        footer = Gtk.Box(
            orientation=Gtk.Orientation.HORIZONTAL,
            margin_top=6, margin_bottom=10, margin_start=10, margin_end=14
        )
        footer.set_halign(Gtk.Align.END)
        btn_flatpak_repos = Gtk.Button(label="Flatpak Repos")
        btn_flatpak_repos.set_tooltip_text("Flathub (und optional Flathub-Beta) zu Flatpak hinzufügen")
        btn_flatpak_repos.add_css_class("suggested-action")
        btn_flatpak_repos.connect("clicked", self._on_add_flatpak_repos)
        footer.append(btn_flatpak_repos)

        right_container.append(footer)
        return right_container

    # ------------------------------ Kacheln ------------------------------
    def _create_app_row(self, app: App) -> Gtk.Widget:
        card = Gtk.Box(
            orientation=Gtk.Orientation.VERTICAL,
            spacing=10,
            css_classes=["card"],
            width_request=250,
            height_request=180,
            margin_top=8, margin_bottom=8, margin_start=8, margin_end=8
        )

        icon_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8, valign=Gtk.Align.CENTER, vexpand=True)
        icon = Gtk.Image.new_from_icon_name(app.icon)
        icon.set_pixel_size(64)
        if not self._icon_exists(app.icon):
            icon.set_from_icon_name(self._get_fallback_icon(app.category))
        icon_box.append(icon)

        name_label = Gtk.Label(label=app.name, css_classes=["title-3"], halign=Gtk.Align.CENTER, ellipsize=3)
        icon_box.append(name_label)

        desc_label = Gtk.Label(
            label=app.description,
            css_classes=["caption"],
            halign=Gtk.Align.CENTER,
            ellipsize=3,
            lines=2,
            wrap=True,
            justify=Gtk.Justification.CENTER
        )
        icon_box.append(desc_label)
        card.append(icon_box)

        source_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=4, halign=Gtk.Align.CENTER, margin_bottom=10)
        if app.xbps_name:
            badge = Gtk.Label(label="XBPS", css_classes=["pill"])
            source_box.append(badge)
        if app.flatpak_id:
            badge = Gtk.Label(label="Flatpak", css_classes=["pill", "accent"])
            source_box.append(badge)
        if app.script_path and not app.xbps_name and not app.flatpak_id:
            badge = Gtk.Label(label="Skript", css_classes=["pill"])
            source_box.append(badge)
        card.append(source_box)

        button = Gtk.Button(child=card)
        button.connect("clicked", self._on_app_selected, app)
        return button

    def _get_fallback_icon(self, category: str) -> str:
        icons = {
            "Büro": "accessories-text-editor",
            "Grafik": "applications-graphics",
            "Multimedia": "applications-multimedia",
            "Internet": "applications-internet",
            "Gaming": "applications-games",
            "Tools": "applications-development",
            "System": "applications-system"
        }
        return icons.get(category, "application-x-executable")

    # --------------------------- Detail-Ansicht ---------------------------
    def _on_app_selected(self, _button, app: App):
        self.detail_scroll.set_child(self._build_detail_page(app))
        self.main_stack.set_visible_child_name("details")
        self._main_title.set_label(app.name)
        self.back_button.set_visible(True)

    def _on_back_clicked(self, _button):
        self.main_stack.set_visible_child_name("grid")
        self._main_title.set_label("Void Software Store")
        self.back_button.set_visible(False)
        self._refresh_cache_and_update()

    def _build_detail_page(self, app: App) -> Gtk.Widget:
        clamp = Adw.Clamp(maximum_size=810, margin_start=24, margin_end=24)
        main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=24, margin_top=24, margin_bottom=24)
        clamp.set_child(main_box)

        header_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=20)
        icon = Gtk.Image.new_from_icon_name(app.icon)
        icon.set_pixel_size(96)
        if not self._icon_exists(app.icon):
            icon.set_from_icon_name(self._get_fallback_icon(app.category))
        header_box.append(icon)

        title_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6, valign=Gtk.Align.CENTER)
        title_box.append(Gtk.Label(label=app.name, xalign=0, css_classes=["title-1"]))
        title_box.append(Gtk.Label(label=app.description, xalign=0, wrap=True, css_classes=["body"]))
        header_box.append(title_box)
        main_box.append(header_box)

        group = Adw.PreferencesGroup(title="Installationsquellen")
        main_box.append(group)

        if app.xbps_name:
            is_installed = self._is_installed_xbps(app.xbps_name)
            row = Adw.ActionRow(title="XBPS Paket", subtitle="Void Linux Paket")
            btn = Gtk.Button(
                label="Entfernen" if is_installed else "Installieren",
                css_classes=["destructive-action" if is_installed else "suggested-action"]
            )
            handler = self._on_uninstall_xbps if is_installed else self._on_install_xbps
            btn.connect("clicked", handler, app)
            row.add_suffix(btn)
            row.set_activatable_widget(btn)
            group.add(row)

        if app.flatpak_id:
            is_installed = self._is_installed_flatpak(app.flatpak_id)
            row = Adw.ActionRow(title="Flatpak", subtitle="Containerisierte Anwendung von Flathub")
            btn = Gtk.Button(
                label="Entfernen" if is_installed else "Installieren",
                css_classes=["destructive-action" if is_installed else "suggested-action"]
            )
            handler = self._on_uninstall_flatpak if is_installed else self._on_install_flatpak
            btn.connect("clicked", handler, app)
            row.add_suffix(btn)
            row.set_activatable_widget(btn)
            group.add(row)

        if app.script_path:
            exists = os.path.exists(app.script_path)
            row = Adw.ActionRow(
                title="Benutzerdefiniertes Skript",
                subtitle=(app.script_path if exists else f"{app.script_path} (nicht gefunden)")
            )
            btn = Gtk.Button(label="Ausführen", css_classes=["warning-action"])
            btn.set_sensitive(exists)
            btn.set_tooltip_text("Skript im Terminal ausführen")
            btn.connect("clicked", self._on_run_script, app)
            row.add_suffix(btn)
            row.set_activatable_widget(btn)
            group.add(row)

        return clamp

    def _icon_exists(self, icon_name: str) -> bool:
        if icon_name in self._icon_cache:
            return self._icon_cache[icon_name]
        try:
            ok = Gtk.IconTheme.get_for_display(self.get_display()).has_icon(icon_name)
            self._icon_cache[icon_name] = ok
            return ok
        except Exception:
            return False

    # ----------------------------- Suche/Filter -----------------------------
    def _on_search_changed(self, _search_entry):
        if hasattr(self, '_search_source_id'):
            GLib.source_remove(self._search_source_id)
        self._search_source_id = GLib.timeout_add(300, self._apply_search)

    def _apply_search(self):
        self.search_term = self.search_entry.get_text().lower().strip()
        self._filter_apps()
        if len(self.search_term) >= 2:
            self._search_external_packages()
        return False

    def _search_external_packages(self):
        def search_thread():
            results = []
            try:
                res = subprocess.run(['xbps-query', '-Rs', f'*{self.search_term}*'],
                                     capture_output=True, text=True, timeout=10)
                if res.returncode == 0 and res.stdout:
                    results.extend(self._parse_xbps_results(res.stdout))

                res = subprocess.run(['flatpak', 'search', self.search_term],
                                     capture_output=True, text=True, timeout=10)
                if res.returncode == 0 and res.stdout:
                    results.extend(self._parse_flatpak_results(res.stdout))

                if results:
                    GLib.idle_add(self._add_search_results, results)
            except Exception:
                pass

        threading.Thread(target=search_thread, daemon=True).start()

    def _parse_xbps_results(self, output: str) -> List[App]:
        apps, existing = [], {app.name.lower() for app in self.apps_data}
        for line in output.strip().split('\n')[:30]:
            if not line or not (line.startswith('[-]') or line.startswith('[*]')):
                continue
            try:
                content = line[4:].strip()
                space_idx = content.find(' ')
                pkg_full, desc = (content[:space_idx], content[space_idx+1:].strip()) if space_idx > 0 else (content, "XBPS Paket")
                mm = re.match(r'^(.+?)-[\d]', pkg_full)
                name = mm.group(1) if mm else pkg_full.split('-')[0]
                if name and name.lower() not in existing:
                    category = self._guess_category(name, desc)
                    apps.append(App(name, desc, category, xbps_name=name, icon=self._guess_icon(name, category)))
            except Exception:
                continue
        return apps

    def _parse_flatpak_results(self, output: str) -> List[App]:
        apps, existing = [], {app.flatpak_id for app in self.apps_data if app.flatpak_id}
        lines = output.strip().split('\n')
        for line in lines[1:31]:
            try:
                parts = [p.strip() for p in line.split('\t')]
                if len(parts) >= 3:
                    name, desc, app_id = parts[:3]
                    if app_id and app_id not in existing:
                        apps.append(App(name, desc, self._guess_category(name, desc), flatpak_id=app_id, icon=app_id))
            except Exception:
                continue
        return apps

    def _guess_category(self, name: str, desc: str) -> str:
        text = (name + " " + desc).lower()
        if any(k in text for k in ['game', 'steam', 'lutris']):
            return "Gaming"
        if any(k in text for k in ['audio', 'video', 'media', 'player']):
            return "Multimedia"
        if any(k in text for k in ['image', 'graphic', 'photo', 'draw']):
            return "Grafik"
        if any(k in text for k in ['http', 'web', 'browser', 'mail']):
            return "Internet"
        if any(k in text for k in ['dev', 'lib', 'code', 'git', 'editor']):
            return "Tools"
        if any(k in text for k in ['system', 'kernel', 'driver']):
            return "System"
        return "Büro"

    def _guess_icon(self, name: str, category: str) -> str:
        # simple fallback: wenn nix besseres da ist
        return self._get_fallback_icon(category)

    def _add_search_results(self, packages: List[App]):
        current_ids = {app.name.lower() for app in self.filtered_apps}
        new_apps = [pkg for pkg in packages if pkg.name.lower() not in current_ids]
        if new_apps:
            self.filtered_apps.extend(new_apps)
            self._update_app_list()

    def _on_category_selected(self, _listbox, row):
        if not row:
            return
        self.current_category = row.get_name()
        self.search_entry.set_text("")
        self._filter_apps()

    def _filter_apps(self):
        term = self.search_term

        if self.current_category == "Installiert":
            source_apps = [app for app in self.apps_data if self._is_installed(app)]
        elif self.current_category == "Alle":
            source_apps = self.apps_data
        else:
            source_apps = [app for app in self.apps_data if app.category == self.current_category]

        if term:
            self.filtered_apps = [app for app in source_apps if term in app.name.lower() or term in app.description.lower()]
        else:
            self.filtered_apps = source_apps

        self._update_app_list()

    def _update_app_list(self):
        if not hasattr(self, 'apps_flowbox'):
            return
        while child := self.apps_flowbox.get_child_at_index(0):
            self.apps_flowbox.remove(child)
        for app in self.filtered_apps:
            self.apps_flowbox.append(self._create_app_row(app))

    # ----------------------- Installations-Checks -----------------------
    def _is_installed(self, app: App) -> bool:
        return (app.xbps_name and self._is_installed_xbps(app.xbps_name)) or \
               (app.flatpak_id and self._is_installed_flatpak(app.flatpak_id))

    def _is_installed_xbps(self, names: str) -> bool:
        return self.install_cache.ready and all(n.lower() in self.install_cache.xbps for n in names.split())

    def _is_installed_flatpak(self, app_id: str) -> bool:
        return self.install_cache.ready and app_id in self.install_cache.flatpaks

    def _refresh_cache_and_update(self):
        def refresh():
            self.install_cache.refresh()
            GLib.idle_add(self._filter_apps)
        threading.Thread(target=refresh, daemon=True).start()

    # ----------------------- Terminal-Handling -----------------------
    def _get_terminal_command(self):
        if shutil.which('gnome-terminal'):
            return ('gnome-terminal', 'argv')
        if shutil.which('konsole'):
            return ('konsole', 'argv')
        if shutil.which('xfce4-terminal'):
            return ('xfce4-terminal', 'string')
        if shutil.which('xterm'):
            return ('xterm', 'argv')
        return (None, 'argv')

    def _run_in_terminal(self, cmd_argv: List[str]):
        term, mode = self._get_terminal_command()
        try:
            if term is None:
                subprocess.Popen(cmd_argv)
            elif term == 'gnome-terminal':
                subprocess.Popen([term, '--'] + cmd_argv)
            elif term == 'konsole':
                subprocess.Popen([term, '-e'] + cmd_argv)
            elif term == 'xterm':
                subprocess.Popen([term, '-e'] + cmd_argv)
            elif term == 'xfce4-terminal':
                one_string = ' '.join(shlex.quote(x) for x in cmd_argv)
                subprocess.Popen([term, '--title=Task', '--hold', '-e', one_string])
        finally:
            GLib.timeout_add(3000, self._refresh_cache_and_update)

    # ----------------------- Install/Uninstall Aktionen -----------------------
    def _on_install_xbps(self, _, app):
        self._run_in_terminal(['sudo', 'xbps-install', '-S'] + app.xbps_name.split())

    def _on_uninstall_xbps(self, _, app):
        self._run_in_terminal(['sudo', 'xbps-remove', '-R'] + app.xbps_name.split())

    def _on_install_flatpak(self, _, app):
        self._run_in_terminal(['flatpak', 'install', '-y', 'flathub', app.flatpak_id])

    def _on_uninstall_flatpak(self, _, app):
        self._run_in_terminal(['flatpak', 'uninstall', '-y', app.flatpak_id])

    def _on_run_script(self, _, app):
        if not app.script_path:
            self._show_error("Kein Skriptpfad gesetzt.")
            return
        if not os.path.exists(app.script_path):
            self._show_error(f"Skript nicht gefunden:\n{app.script_path}")
            return

        try:
            os.chmod(app.script_path, os.stat(app.script_path).st_mode | 0o111)
        except Exception:
            pass

        if app.needs_root:
            self._run_in_terminal(['sudo', '/bin/bash', app.script_path])
        else:
            self._run_in_terminal(['/bin/bash', app.script_path])

    def _on_update_clicked(self, _):
        self._run_in_terminal(['topgrade'])

    # ----------------------- Flatpak-Repos hinzufügen -----------------------
    def _on_add_flatpak_repos(self, _button):
        shell_script = (
            "(command -v flatpak >/dev/null 2>&1) || sudo xbps-install -S flatpak; "
            "flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo; "
            "flatpak remote-add --if-not-exists flathub-beta https://flathub.org/beta-repo/flathub-beta.flatpakrepo; "
            "echo 'Fertig: Flatpak-Repositories sind gesetzt.'; "
            "read -n 1 -p 'Taste drücken zum Schließen...' || true"
        )
        cmd = ['/bin/bash', '-lc', shell_script]
        self._run_in_terminal(cmd)

    # ----------------------- Hilfen -----------------------
    def _show_error(self, text: str):
        try:
            dialog = Adw.MessageDialog.new(self, "Fehler", text)
            dialog.add_response("ok", "OK")
            dialog.set_default_response("ok")
            dialog.run_async(None, None, None)
        except Exception:
            print("ERROR:", text)

# ------------------------------ main() ------------------------------
def main():
    app = VoidSoftwareStore()
    return app.run(sys.argv)

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

