// rakuos-software-cosmic — libcosmic software center frontend

mod pages;

use cosmic::action;
use cosmic::app::{Core, Settings, Task};
use cosmic::iced::widget::scrollable::Viewport;
use cosmic::iced::window;
use cosmic::iced::{time, Alignment, Length, Size, Subscription};
use cosmic::iced_core::layout::Limits as LayoutLimits;
use cosmic::{executor, widget, Application, ApplicationExt, Element};
use pages::scroll_to_top;
use rakuos_home::HomeApp;
use rakuos_packages::{NativeApp, SourceOption};
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::path::PathBuf;
use std::sync::{Arc, atomic::{AtomicU32, Ordering}};
use std::time::Duration;

const APP_ID: &str = "org.rakuos.Software";
const MAX_GRID_WIDTH: f32 = 1600.0;
const ICON_SIZE_CARD: u16 = 48;
const ICON_SIZE_DETAIL: u16 = 96;
const CATEGORY_PAGE_SIZE: usize = 60;
const WEBAPP_PAGE_SIZE: usize = 60;

#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum NavPage {
    #[default]
    Home,
    Category {
        title: &'static str,
        cat: &'static str,
        icon: &'static str,
        sub: bool,
    },
    Installed,
    Updates,
    WebApps,
    System,
    Settings,
}

#[derive(Clone, Copy)]
struct CategoryGroup {
    title: &'static str,
    cat: &'static str,
    icon: &'static str,
    subs: &'static [CategorySub],
}

#[derive(Clone, Copy)]
struct CategorySub {
    title: &'static str,
    cat: &'static str,
}

const CATEGORY_TREE: &[CategoryGroup] = &[
    CategoryGroup {
        title: "Games",
        cat: "Game",
        icon: "applications-games-symbolic",
        subs: &[
            CategorySub { title: "Action", cat: "ActionGame" },
            CategorySub { title: "Adventure", cat: "AdventureGame" },
            CategorySub { title: "Arcade", cat: "ArcadeGame" },
            CategorySub { title: "Board Games", cat: "BoardGame" },
            CategorySub { title: "Card Games", cat: "CardGame" },
            CategorySub { title: "Emulators", cat: "Emulator" },
            CategorySub { title: "Kids", cat: "KidsGame" },
            CategorySub { title: "Logic", cat: "LogicGame" },
            CategorySub { title: "Role Playing", cat: "RolePlaying" },
            CategorySub { title: "Shooter", cat: "Shooter" },
            CategorySub { title: "Simulation", cat: "Simulation" },
            CategorySub { title: "Sports", cat: "SportsGame" },
            CategorySub { title: "Strategy", cat: "StrategyGame" },
        ],
    },
    CategoryGroup {
        title: "Internet",
        cat: "Network",
        icon: "applications-internet-symbolic",
        subs: &[
            CategorySub { title: "Web Browsers", cat: "WebBrowser" },
            CategorySub { title: "Email", cat: "Email" },
            CategorySub { title: "Chat & IM", cat: "InstantMessaging" },
            CategorySub { title: "File Sharing", cat: "FileTransfer" },
            CategorySub { title: "News & RSS", cat: "News" },
            CategorySub { title: "Remote Access", cat: "RemoteAccess" },
        ],
    },
    CategoryGroup {
        title: "Audio & Video",
        cat: "AudioVideo",
        icon: "applications-multimedia-symbolic",
        subs: &[
            CategorySub { title: "Music", cat: "Music" },
            CategorySub { title: "Video", cat: "Video" },
            CategorySub { title: "Editors", cat: "AudioVideoEditing" },
            CategorySub { title: "MIDI", cat: "Midi" },
            CategorySub { title: "TV", cat: "TV" },
            CategorySub { title: "Recorders", cat: "Recorder" },
            CategorySub { title: "Mixers", cat: "Mixer" },
        ],
    },
    CategoryGroup {
        title: "Graphics",
        cat: "Graphics",
        icon: "applications-graphics-symbolic",
        subs: &[
            CategorySub { title: "2D Editors", cat: "2DGraphics" },
            CategorySub { title: "3D Editors", cat: "3DGraphics" },
            CategorySub { title: "Photography", cat: "Photography" },
            CategorySub { title: "Viewers", cat: "Viewer" },
            CategorySub { title: "Publishing", cat: "Publishing" },
            CategorySub { title: "Scanning", cat: "Scanning" },
        ],
    },
    CategoryGroup {
        title: "Productivity",
        cat: "Office",
        icon: "applications-office-symbolic",
        subs: &[
            CategorySub { title: "Office Suite", cat: "WordProcessor" },
            CategorySub { title: "Spreadsheets", cat: "Spreadsheet" },
            CategorySub { title: "Presentation", cat: "Presentation" },
            CategorySub { title: "Calendar", cat: "Calendar" },
            CategorySub { title: "Finance", cat: "Finance" },
            CategorySub { title: "Contacts", cat: "ContactManagement" },
            CategorySub { title: "Project Mgmt", cat: "ProjectManagement" },
        ],
    },
    CategoryGroup {
        title: "Development",
        cat: "Development",
        icon: "applications-development-symbolic",
        subs: &[
            CategorySub { title: "IDEs", cat: "IDE" },
            CategorySub { title: "Version Control", cat: "RevisionControl" },
            CategorySub { title: "Debuggers", cat: "Debugger" },
            CategorySub { title: "Web Dev", cat: "WebDevelopment" },
            CategorySub { title: "Database", cat: "Database" },
            CategorySub { title: "Data Viz", cat: "DataVisualization" },
        ],
    },
    CategoryGroup {
        title: "System",
        cat: "System",
        icon: "applications-system-symbolic",
        subs: &[
            CategorySub { title: "File Managers", cat: "FileManager" },
            CategorySub { title: "Terminal", cat: "TerminalEmulator" },
            CategorySub { title: "Monitors", cat: "Monitor" },
            CategorySub { title: "Security", cat: "Security" },
            CategorySub { title: "Settings", cat: "Settings" },
            CategorySub { title: "Package Mgmt", cat: "PackageManager" },
        ],
    },
    CategoryGroup {
        title: "Utilities",
        cat: "Utility",
        icon: "applications-utilities-symbolic",
        subs: &[
            CategorySub { title: "Archiving", cat: "Archiving" },
            CategorySub { title: "Calculators", cat: "Calculator" },
            CategorySub { title: "Clocks", cat: "Clock" },
            CategorySub { title: "Text Editors", cat: "TextEditor" },
            CategorySub { title: "File Tools", cat: "FileTools" },
            CategorySub { title: "Dictionary", cat: "Dictionary" },
        ],
    },
    CategoryGroup {
        title: "Science",
        cat: "Science",
        icon: "applications-science-symbolic",
        subs: &[
            CategorySub { title: "Astronomy", cat: "Astronomy" },
            CategorySub { title: "Chemistry", cat: "Chemistry" },
            CategorySub { title: "Electronics", cat: "Electronics" },
            CategorySub { title: "Engineering", cat: "Engineering" },
            CategorySub { title: "Geography", cat: "Geography" },
            CategorySub { title: "Physics", cat: "Physics" },
        ],
    },
    CategoryGroup {
        title: "Education",
        cat: "Education",
        icon: "applications-education-symbolic",
        subs: &[
            CategorySub { title: "Languages", cat: "Languages" },
            CategorySub { title: "Mathematics", cat: "Math" },
            CategorySub { title: "Science", cat: "Science" },
            CategorySub { title: "Literature", cat: "Literature" },
            CategorySub { title: "Geography", cat: "Geography" },
            CategorySub { title: "Kids", cat: "KidsGame" },
        ],
    },
];

impl NavPage {
    fn title(self) -> &'static str {
        match self {
            Self::Home => "Home",
            Self::Category { title, .. } => title,
            Self::Installed => "Installed Apps",
            Self::Updates => "Updates",
            Self::WebApps => "Web Apps",
            Self::System => "System",
            Self::Settings => "Settings",
        }
    }

    fn icon(self) -> widget::icon::Icon {
        let name = match self {
            Self::Home => "go-home-symbolic",
            Self::Category { icon, sub, .. } => {
                if sub {
                    "go-next-symbolic"
                } else {
                    icon
                }
            }
            Self::Installed => "emblem-ok-symbolic",
            Self::Updates => "software-update-available-symbolic",
            Self::WebApps => "applications-internet-symbolic",
            Self::System => "computer-symbolic",
            Self::Settings => "preferences-system-symbolic",
        };
        widget::icon::from_name(name).size(16).into()
    }

    fn category_ids(self) -> Vec<&'static str> {
        match self {
            Self::Category { cat, sub: true, .. } => vec![cat],
            Self::Category { cat, sub: false, .. } => vec![cat],
            _ => Vec::new(),
        }
    }

    fn is_subcategory(self) -> bool {
        matches!(self, Self::Category { sub: true, .. })
    }

    fn is_top_category(self) -> bool {
        matches!(self, Self::Category { sub: false, .. })
    }

    fn category_id(self) -> Option<&'static str> {
        match self {
            Self::Category { cat, .. } => Some(cat),
            _ => None,
        }
    }
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum HomeSection {
    Picks,
    Popular,
    Updated,
    New,
}

impl HomeSection {
    pub(crate) fn title(self) -> &'static str {
        match self {
            Self::Picks => "Editor's Picks",
            Self::Popular => "Popular Apps",
            Self::Updated => "Recently Updated",
            Self::New => "New Apps",
        }
    }
}

#[derive(Clone, Debug)]
pub enum LoadKind {
    Home,
    Category(NavPage),
    Installed,
    Search(String),
    WebApps,
    WebAppSearch(String),
}

#[derive(Clone, Debug)]
pub enum OperationKind {
    Install,
    Remove,
}

#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum InstalledTab {
    #[default]
    Rpm,
    Flatpak,
    AppImages,
    WebApps,
}

#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum SettingsTab {
    #[default]
    Updates,
    Flatpak,
    Firmware,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum SystemDialog {
    Rollback,
    SwitchConfirm,
    SwitchProgress,
    OverlayReset,
    KernelArgs,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct DesktopImage {
    pub label: &'static str,
    pub image_name: &'static str,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ImageBranch {
    pub label: &'static str,
    pub tag: &'static str,
}

pub const DESKTOP_IMAGES: &[DesktopImage] = &[
    DesktopImage { label: "KDE Plasma", image_name: "rakuos-kde" },
    DesktopImage { label: "GNOME", image_name: "rakuos-gnome" },
    DesktopImage { label: "COSMIC", image_name: "rakuos-cosmic" },
];

pub const IMAGE_BRANCHES: &[ImageBranch] = &[
    ImageBranch { label: "Stable", tag: "latest" },
    ImageBranch { label: "Staging", tag: "staging" },
    ImageBranch { label: "Rawhide", tag: "rawhide" },
];

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SettingsData {
    pub update_interval: u32,
    pub auto_check_packages: bool,
    pub auto_check_flatpak: bool,
    pub auto_check_image: bool,
    pub auto_check_appimages: bool,
    pub auto_update: bool,
}

impl Default for SettingsData {
    fn default() -> Self {
        Self {
            update_interval: 1440,
            auto_check_packages: true,
            auto_check_flatpak: true,
            auto_check_image: true,
            auto_check_appimages: true,
            auto_update: false,
        }
    }
}

#[derive(Clone, Debug, Default)]
pub struct SystemPageData {
    status: rakuos_updates::SystemStatus,
    overlay: rakuos_updates::OverlayStatus,
}

#[derive(Clone, Debug, Default)]
pub struct FirmwareData {
    available: bool,
    remotes: Vec<rakuos_firmware::FwupdRemote>,
    vendor_dirs: Vec<rakuos_firmware::FwupdRemote>,
    updates: Vec<serde_json::Value>,
}

#[derive(Clone, Debug, Default)]
pub struct InstalledData {
    rpm_apps: Vec<NativeApp>,
    flatpak_apps: Vec<NativeApp>,
    flatpak_runtimes: Vec<rakuos_packages::FlatpakRuntime>,
    app_images: Vec<NativeApp>,
    web_apps: Vec<NativeApp>,
}

#[derive(Clone, Debug)]
pub struct LocalFileState {
    pub(crate) path: String,
    pub(crate) kind: String,
    pub(crate) info: Option<serde_json::Value>,
    pub(crate) loading: bool,
    pub(crate) installing: bool,
    pub(crate) success: bool,
    pub(crate) status: String,
    pub(crate) log: String,
}

#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct UpdatesData {
    #[serde(default)]
    packages: Vec<serde_json::Value>,
    #[serde(default)]
    flatpak: Vec<serde_json::Value>,
    #[serde(default)]
    webapps: Vec<serde_json::Value>,
    #[serde(default)]
    appimages: Vec<serde_json::Value>,
    #[serde(default)]
    image_available: bool,
    #[serde(default)]
    reboot_required: bool,
    #[serde(default)]
    image_info: serde_json::Value,
    #[serde(default)]
    total: usize,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub enum UpdateTarget {
    All,
    Packages,
    Package(String),
    Flatpak,
    FlatpakApp(String),
    FlatpakRuntime { app_id: String, branch: String },
    FlatpakRuntimeInstall { remote: String, app_id: String, branch: String },
    WebApps,
    WebApp(String),
    AppImage { id: String, url: String },
    Image,
}

pub struct SoftwareApp {
    core: Core,
    nav_model: widget::nav_bar::Model,
    scrollable_id: widget::Id,
    search_id: widget::Id,
    search_active: bool,
    search_input: String,
    loading: bool,
    operation_running: bool,
    status: String,
    local_file: Option<LocalFileState>,
    expanded_category: Option<&'static str>,
    home_sections: Vec<(HomeSection, Vec<HomeApp>)>,
    page_apps: Vec<NativeApp>,
    page_visible_count: usize,
    installed_data: InstalledData,
    installed_tab: InstalledTab,
    webapp_catalog: Vec<NativeApp>,
    webapp_visible_count: usize,
    webapp_custom_open: bool,
    webapp_custom_running: bool,
    webapp_custom_name: String,
    webapp_custom_url: String,
    webapp_custom_description: String,
    webapp_custom_category: String,
    webapp_custom_icon_source: String,
    webapp_custom_status: String,
    selected: Option<NativeApp>,
    selected_source_index: usize,
    source_menu_open: bool,
    addon_list: Vec<serde_json::Value>,
    addon_dialog_open: bool,
    screenshot_index: usize,
    system_data: Option<SystemPageData>,
    system_loading: bool,
    system_status: String,
    system_success: bool,
    system_reboot_required: bool,
    system_dialog: Option<SystemDialog>,
    system_switch_de: usize,
    system_switch_branch: usize,
    system_switch_label: String,
    system_switch_repo: String,
    system_switch_tag: String,
    system_switch_log: String,
    system_switch_running: bool,
    system_switch_success: bool,
    kargs_cmdline: String,
    kargs_user: String,
    kargs_input: String,
    kargs_running: bool,
    kargs_status: String,
    kargs_success: bool,
    settings_tab: SettingsTab,
    settings_data: SettingsData,
    settings_saved: bool,
    bootc_autoupdate: bool,
    flatpak_remotes: Vec<rakuos_flatpak::FlatpakRemote>,
    flatpak_has_flathub: bool,
    flatpak_status: String,
    flatpak_add_remote_open: bool,
    flatpak_new_name: String,
    flatpak_new_url: String,
    flatpak_new_system: bool,
    firmware_data: FirmwareData,
    firmware_status: String,
    firmware_refreshing: bool,
    reviews: Vec<rakuos_reviews::Review>,
    reviews_loading: bool,
    reviews_page: usize,
    review_popup_open: bool,
    review_rating: i32,
    review_name: String,
    review_summary: String,
    review_body: String,
    review_submit_running: bool,
    review_status: String,
    appimage_update_source: String,
    appimage_update_url: String,
    appimage_update_pattern: String,
    appimage_settings_status: String,
    updates_data: Option<UpdatesData>,
    updates_loading: bool,
    updates_checking: bool,
    updates_updating: bool,
    updates_status: String,
    updates_completed: HashSet<String>,
    updates_active: Option<UpdateTarget>,
    updates_image_done: bool,
    updates_image_progress: f32,
    updates_image_progress_arc: Arc<AtomicU32>,
    updates_cache_mtime: Option<std::time::SystemTime>,
    overlay_update_running: bool,
    tray_mode: bool,
    tray_window_id: Option<window::Id>,
    // Queued install/remove operations: (display_name, app_id, source, kind, icon_path, icon_url)
    install_queue: std::collections::VecDeque<(String, String, SourceOption, OperationKind, String, String)>,
    // Display name, icon, and progress (0.0–1.0) of the currently-running install/remove.
    active_op_name: String,
    active_op_is_remove: bool,
    active_op_progress: f32,
    active_op_icon_path: String,
    active_op_icon_url: String,
}

#[derive(Clone, Debug)]
pub enum Message {
    HomeLoaded(Vec<(HomeSection, Vec<HomeApp>)>),
    InstalledLoaded(InstalledData),
    Loaded(LoadKind, Vec<NativeApp>),
    LoadFailed(String),
    LocalFileLoaded(serde_json::Value),
    LocalFileFailed(String),
    CloseLocalFile,
    InstallLocalFile,
    LocalFileInstallFinished(bool, String),
    SearchActivate,
    SearchClear,
    SearchInput(String),
    SearchSubmit(String),
    SelectApp(String),
    SelectInstalledApp(NativeApp),
    SelectedLoaded(Option<NativeApp>),
    SelectNone,
    ToggleSourceMenu,
    SelectSource(usize),
    AddonsLoaded(Vec<serde_json::Value>),
    ToggleAddonsDialog,
    AddonOperation(OperationKind, String, String),
    AddonOperationFinished(bool, String),
    ScreenshotNext,
    ScreenshotPrev,
    SelectScreenshot(usize),
    ReviewsLoaded(Vec<rakuos_reviews::Review>),
    ReviewsPagePrev,
    ReviewsPageNext,
    OpenReviewPopup,
    CloseReviewPopup,
    ReviewRating(i32),
    ReviewNameChanged(String),
    ReviewSummaryChanged(String),
    ReviewBodyChanged(String),
    SubmitReview,
    ReviewSubmitted(Result<(), String>),
    VoteReview(i64, bool),
    AppImageUpdateSourceChanged(String),
    AppImageUpdateUrlChanged(String),
    AppImageUpdatePatternChanged(String),
    SaveAppImageSettings,
    AppImageSettingsSaved(bool, String),
    SystemLoaded(SystemPageData),
    SystemAction(String),
    SystemActionFinished(bool, String),
    OpenSystemDialog(SystemDialog),
    CloseSystemDialog,
    SelectSystemDe(usize),
    SelectSystemBranch(usize),
    PrepareSwitchImage(String, String, String),
    SwitchImage,
    SwitchImageFinished(bool, String),
    OpenKernelArgsDialog,
    KernelArgsInputChanged(String),
    ApplyKernelArgs,
    KernelArgsClear,
    KernelArgsFinished(bool, String),
    RebootSystem,
    SelectSettingsTab(SettingsTab),
    SettingsLoaded(SettingsData),
    SettingsInterval(u32),
    SettingsTogglePackages(bool),
    SettingsToggleFlatpak(bool),
    SettingsToggleImage(bool),
    SettingsToggleAppImages(bool),
    SettingsToggleAutoUpdate(bool),
    SettingsToggleBootcAutoUpdate(bool),
    SaveSettings,
    FlatpakRemotesLoaded(Vec<rakuos_flatpak::FlatpakRemote>, bool),
    AddFlathub,
    OpenFlatpakAddRemote,
    CloseFlatpakAddRemote,
    FlatpakNewNameChanged(String),
    FlatpakNewUrlChanged(String),
    FlatpakNewSystemChanged(bool),
    AddFlatpakRemote,
    ToggleFlatpakRemote(String, bool, bool),
    RemoveFlatpakRemote(String, bool),
    FlatpakActionFinished(bool, String),
    FirmwareLoaded(FirmwareData),
    ToggleFirmwareRemote(String, bool),
    RefreshFirmware,
    FirmwareActionFinished(bool, String),
    PageScrolled(Viewport),
    LoadMoreApps,
    OpenCustomWebApp,
    CloseCustomWebApp,
    CustomWebAppNameChanged(String),
    CustomWebAppUrlChanged(String),
    CustomWebAppDescriptionChanged(String),
    CustomWebAppCategoryChanged(String),
    CustomWebAppIconSourceChanged(String),
    BrowseCustomWebAppIcon,
    CustomWebAppIconSelected(Result<String, String>),
    AddCustomWebApp,
    CustomWebAppFinished(bool, String),
    SelectInstalledTab(InstalledTab),
    Operation(OperationKind),
    OperationFinished(bool, String, Option<NativeApp>),
    UpdatesCacheLoaded(Option<UpdatesData>),
    BootcReconnect(bool),
    CheckUpdates,
    UpdatesChecked(Result<UpdatesData, String>),
    UpdateTarget(UpdateTarget),
    UpdateFinished(UpdateTarget, bool, String),
    DaemonTick,
    WindowCloseRequested(window::Id),
    LaunchUrl(String),
    LaunchWebApp(String),
}

impl Application for SoftwareApp {
    type Executor = executor::Default;
    type Flags = ();
    type Message = Message;

    const APP_ID: &'static str = APP_ID;

    fn core(&self) -> &Core {
        &self.core
    }

    fn core_mut(&mut self) -> &mut Core {
        &mut self.core
    }

    fn init(core: Core, (): ()) -> (Self, Task<Message>) {
        let startup_file = std::env::args()
            .skip(1)
            .find(|arg| !arg.starts_with('-'))
            .map(|arg| normalize_file_arg(&arg))
            .filter(|path| local_file_kind(path).is_some());
        let startup_task = startup_file
            .as_ref()
            .and_then(|path| local_file_kind(path).map(|kind| load_local_file(path.clone(), kind.to_string())));

        let mut app = Self {
            core,
            nav_model: build_nav_model(None, NavPage::Home),
            scrollable_id: widget::Id::new("rakuos-software-cosmic-scroll"),
            search_id: widget::Id::new("rakuos-software-cosmic-search"),
            search_active: false,
            search_input: String::new(),
            loading: true,
            operation_running: false,
            status: String::new(),
            local_file: startup_file.as_ref().and_then(|path| {
                local_file_kind(path).map(|kind| LocalFileState {
                    path: path.clone(),
                    kind: kind.to_string(),
                    info: None,
                    loading: true,
                    installing: false,
                    success: false,
                    status: String::new(),
                    log: String::new(),
                })
            }),
            expanded_category: None,
            home_sections: Vec::new(),
            page_apps: Vec::new(),
            page_visible_count: CATEGORY_PAGE_SIZE,
            installed_data: InstalledData::default(),
            installed_tab: InstalledTab::default(),
            webapp_catalog: Vec::new(),
            webapp_visible_count: WEBAPP_PAGE_SIZE,
            webapp_custom_open: false,
            webapp_custom_running: false,
            webapp_custom_name: String::new(),
            webapp_custom_url: String::new(),
            webapp_custom_description: String::new(),
            webapp_custom_category: "Network".to_string(),
            webapp_custom_icon_source: String::new(),
            webapp_custom_status: String::new(),
            selected: None,
            selected_source_index: 0,
            source_menu_open: false,
            addon_list: Vec::new(),
            addon_dialog_open: false,
            screenshot_index: 0,
            system_data: None,
            system_loading: false,
            system_status: String::new(),
            system_success: false,
            system_reboot_required: false,
            system_dialog: None,
            system_switch_de: 0,
            system_switch_branch: 0,
            system_switch_label: String::new(),
            system_switch_repo: String::new(),
            system_switch_tag: String::new(),
            system_switch_log: String::new(),
            system_switch_running: false,
            system_switch_success: false,
            kargs_cmdline: String::new(),
            kargs_user: String::new(),
            kargs_input: String::new(),
            kargs_running: false,
            kargs_status: String::new(),
            kargs_success: false,
            settings_tab: SettingsTab::default(),
            settings_data: SettingsData::default(),
            settings_saved: false,
            bootc_autoupdate: rakuos_updates::is_bootc_autoupdate_enabled(),
            flatpak_remotes: Vec::new(),
            flatpak_has_flathub: false,
            flatpak_status: String::new(),
            flatpak_add_remote_open: false,
            flatpak_new_name: String::new(),
            flatpak_new_url: String::new(),
            flatpak_new_system: true,
            firmware_data: FirmwareData::default(),
            firmware_status: String::new(),
            firmware_refreshing: false,
            reviews: Vec::new(),
            reviews_loading: false,
            reviews_page: 0,
            review_popup_open: false,
            review_rating: 4,
            review_name: String::new(),
            review_summary: String::new(),
            review_body: String::new(),
            review_submit_running: false,
            review_status: String::new(),
            appimage_update_source: "none".to_string(),
            appimage_update_url: String::new(),
            appimage_update_pattern: String::new(),
            appimage_settings_status: String::new(),
            updates_data: None,
            updates_loading: false,
            updates_checking: false,
            updates_updating: false,
            updates_status: String::new(),
            updates_completed: HashSet::new(),
            updates_active: None,
            updates_image_done: false,
            updates_image_progress: 0.0,
            updates_image_progress_arc: Arc::new(AtomicU32::new(0)),
            updates_cache_mtime: std::fs::metadata(daemon_cache_path())
                .and_then(|m| m.modified())
                .ok(),
            overlay_update_running: std::path::Path::new("/run/rakuos-overlay.lock").exists(),
            tray_mode: std::env::args().any(|arg| arg == "--tray"),
            tray_window_id: None,
            install_queue: std::collections::VecDeque::new(),
            active_op_name: String::new(),
            active_op_is_remove: false,
            active_op_progress: 0.02,
            active_op_icon_path: String::new(),
            active_op_icon_url: String::new(),
        };
        app.set_header_title("RakuOS Software".into());
        let init_task = if let Some(task) = startup_task {
            task
        } else {
            load_home()
        };
        (app, init_task)
    }

    fn nav_model(&self) -> Option<&widget::nav_bar::Model> {
        Some(&self.nav_model)
    }

    fn on_nav_select(&mut self, id: widget::nav_bar::Id) -> Task<Message> {
        self.nav_model.activate(id);
        self.local_file = None;
        self.selected = None;
        self.selected_source_index = 0;
        self.source_menu_open = false;
        self.screenshot_index = 0;
        self.clear_reviews();
        self.search_active = false;
        self.search_input.clear();
        self.loading = true;
        self.status.clear();

        let page = self.active_page();
        if page.is_top_category() {
            self.expanded_category = match (self.expanded_category, page.category_id()) {
                (Some(current), Some(clicked)) if current == clicked => None,
                (_, clicked) => clicked,
            };
            self.nav_model = build_nav_model(self.expanded_category, page);
        }

        if page.is_subcategory() {
            self.expanded_category = parent_category_for(page.category_id().unwrap_or_default());
            self.nav_model = build_nav_model(self.expanded_category, page);
        }

        match page {
            NavPage::Home => load_home(),
            NavPage::Installed => load_installed_page(),
            NavPage::WebApps => load_apps(LoadKind::WebApps),
            NavPage::System if is_live_environment() => {
                self.loading = false;
                self.status = "System management is not available in the live environment.".to_string();
                Task::none()
            }
            NavPage::System => {
                self.system_loading = true;
                self.system_status.clear();
                load_system_page()
            }
            NavPage::Settings => Task::batch(vec![
                load_settings(),
                load_flatpak_remotes(),
                load_firmware_data(),
            ]),
            NavPage::Updates => {
                self.loading = false;
                self.updates_loading = true;
                load_updates_cache()
            }
            page if page.is_subcategory() || page.is_top_category() => load_apps(LoadKind::Category(page)),
            _ => {
                self.loading = false;
                Task::none()
            }
        }
    }

    fn header_start(&self) -> Vec<Element<'_, Message>> {
        vec![if self.search_active {
            widget::text_input::search_input("Search apps...", &self.search_input)
                .id(self.search_id.clone())
                .width(Length::Fixed(300.0))
                .on_clear(Message::SearchClear)
                .on_input(Message::SearchInput)
                .on_submit(Message::SearchSubmit)
                .into()
        } else {
            widget::button::icon(widget::icon::from_name("system-search-symbolic"))
                .padding(8)
                .on_press(Message::SearchActivate)
                .into()
        }]
    }

    fn header_end(&self) -> Vec<Element<'_, Message>> {
        if self.loading || self.operation_running {
            vec![widget::text::caption("Working...").into()]
        } else {
            Vec::new()
        }
    }

    fn subscription(&self) -> Subscription<Message> {
        let interval = if self.updates_updating {
            Duration::from_millis(500)
        } else {
            Duration::from_secs(2)
        };
        time::every(interval).map(|_| Message::DaemonTick)
    }

    fn on_close_requested(&self, id: window::Id) -> Option<Message> {
        Some(Message::WindowCloseRequested(id))
    }

    fn on_app_exit(&mut self) -> Option<Message> {
        let _ = std::fs::remove_file(pid_file());
        None
    }

    fn update(&mut self, message: Message) -> Task<Message> {
        match message {
            Message::Loaded(kind, apps) => {
                self.loading = false;
                match kind {
                    LoadKind::Home => {
                        self.page_apps.clear();
                    }
                    LoadKind::Category(_) | LoadKind::Installed | LoadKind::Search(_) => {
                        self.page_visible_count = CATEGORY_PAGE_SIZE.min(apps.len());
                        self.page_apps = apps;
                    }
                    LoadKind::WebApps | LoadKind::WebAppSearch(_) => {
                        self.webapp_visible_count = WEBAPP_PAGE_SIZE.min(apps.len());
                        self.webapp_catalog = apps;
                    }
                }
                scroll_to_top(self.scrollable_id.clone())
            }
            Message::HomeLoaded(sections) => {
                self.loading = false;
                self.home_sections = sections;
                self.page_apps.clear();
                scroll_to_top(self.scrollable_id.clone())
            }
            Message::InstalledLoaded(data) => {
                self.loading = false;
                self.installed_data = data;
                self.page_apps.clear();
                scroll_to_top(self.scrollable_id.clone())
            }
            Message::LoadFailed(error) => {
                self.loading = false;
                self.status = error;
                Task::none()
            }
            Message::LocalFileLoaded(info) => {
                if let Some(local) = self.local_file.as_mut() {
                    local.info = Some(info);
                    local.loading = false;
                    local.status.clear();
                }
                self.loading = false;
                scroll_to_top(self.scrollable_id.clone())
            }
            Message::LocalFileFailed(error) => {
                if let Some(local) = self.local_file.as_mut() {
                    local.loading = false;
                    local.status = error;
                }
                self.loading = false;
                Task::none()
            }
            Message::CloseLocalFile => {
                self.local_file = None;
                self.status.clear();
                if self.home_sections.is_empty() {
                    self.loading = true;
                    load_home()
                } else {
                    Task::none()
                }
            }
            Message::InstallLocalFile => {
                let Some(local) = self.local_file.as_mut() else {
                    return Task::none();
                };
                let action = local
                    .info
                    .as_ref()
                    .and_then(|info| info["install_action"].as_str())
                    .unwrap_or("install")
                    .to_string();
                let name = local
                    .info
                    .as_ref()
                    .and_then(|info| info["name"].as_str())
                    .unwrap_or_default()
                    .to_string();
                local.installing = true;
                local.success = false;
                local.status = "Installing...".to_string();
                local.log.clear();
                install_local_file(local.path.clone(), local.kind.clone(), action, name)
            }
            Message::LocalFileInstallFinished(ok, log) => {
                if let Some(local) = self.local_file.as_mut() {
                    local.installing = false;
                    local.success = ok;
                    local.status = if ok {
                        "Installed successfully.".to_string()
                    } else {
                        "Installation failed.".to_string()
                    };
                    local.log = log;
                }
                Task::none()
            }
            Message::SearchActivate => {
                self.search_active = true;
                widget::text_input::focus(self.search_id.clone())
            }
            Message::SearchClear => {
                self.search_input.clear();
                self.search_active = false;
                self.selected = None;
                self.loading = true;
                match self.active_page() {
                    NavPage::Home => load_home(),
                    page if page.is_subcategory() || page.is_top_category() => load_apps(LoadKind::Category(page)),
                    NavPage::Installed => load_installed_page(),
                    NavPage::WebApps => load_apps(LoadKind::WebApps),
                    _ => {
                        self.loading = false;
                        Task::none()
                    }
                }
            }
            Message::SearchInput(input) => {
                self.search_input = input;
                Task::none()
            }
            Message::SearchSubmit(input) => {
                self.selected = None;
                self.loading = true;
                self.status.clear();
                if self.active_page() == NavPage::WebApps {
                    load_apps(LoadKind::WebAppSearch(input))
                } else {
                    load_apps(LoadKind::Search(input))
                }
            }
            Message::SelectApp(id) => {
                self.local_file = None;
                self.loading = true;
                self.selected_source_index = 0;
                self.source_menu_open = false;
                self.screenshot_index = 0;
                self.clear_reviews();
                Task::perform(
                    async move { rakuos_packages::get_app_by_id(&id).ok().flatten() },
                    |app| action::app(Message::SelectedLoaded(app)),
                )
            }
            Message::SelectInstalledApp(app) => {
                self.local_file = None;
                self.selected_source_index = preferred_source_index(&app);
                self.source_menu_open = false;
                self.addon_dialog_open = false;
                self.screenshot_index = 0;
                self.load_appimage_settings_from_app(&app);
                let reviews_task = if app_supports_reviews(&app, self.selected_source_index) {
                    self.start_reviews();
                    load_reviews(app.id.clone())
                } else {
                    self.clear_reviews();
                    Task::none()
                };
                let src = selected_source(&app, self.selected_source_index);
                let addon_id = if src.source == "flatpak" { src.id.clone() } else { app.id.clone() };
                let addon_src = src.source.clone();
                let addons_task = Task::perform(
                    async move { rakuos_packages::get_addons_for_app(&addon_id, &addon_src).unwrap_or_default() },
                    |addons| action::app(Message::AddonsLoaded(addons)),
                );
                self.selected = Some(app);
                Task::batch(vec![
                    scroll_to_top(self.scrollable_id.clone()),
                    reviews_task,
                    addons_task,
                ])
            }
            Message::SelectedLoaded(app) => {
                self.loading = false;
                if let Some(app) = app {
                    self.selected_source_index = preferred_source_index(&app);
                    self.source_menu_open = false;
                    self.addon_dialog_open = false;
                    self.screenshot_index = 0;
                    self.load_appimage_settings_from_app(&app);
                    let reviews_task = if app_supports_reviews(&app, self.selected_source_index) {
                        self.start_reviews();
                        load_reviews(app.id.clone())
                    } else {
                        self.clear_reviews();
                        Task::none()
                    };
                    let src = selected_source(&app, self.selected_source_index);
                    let addon_id = if src.source == "flatpak" { src.id.clone() } else { app.id.clone() };
                    let addon_src = src.source.clone();
                    let addons_task = Task::perform(
                        async move { rakuos_packages::get_addons_for_app(&addon_id, &addon_src).unwrap_or_default() },
                        |addons| action::app(Message::AddonsLoaded(addons)),
                    );
                    self.selected = Some(app);
                    return Task::batch(vec![
                        scroll_to_top(self.scrollable_id.clone()),
                        reviews_task,
                        addons_task,
                    ]);
                }
                scroll_to_top(self.scrollable_id.clone())
            }
            Message::SelectNone => {
                self.local_file = None;
                self.selected = None;
                self.selected_source_index = 0;
                self.source_menu_open = false;
                self.addon_list = Vec::new();
                self.addon_dialog_open = false;
                self.screenshot_index = 0;
                self.clear_reviews();
                scroll_to_top(self.scrollable_id.clone())
            }
            Message::ToggleSourceMenu => {
                self.source_menu_open = !self.source_menu_open;
                Task::none()
            }
            Message::SelectSource(index) => {
                self.selected_source_index = index;
                self.source_menu_open = false;
                self.addon_dialog_open = false;
                self.screenshot_index = 0;
                if let Some(app) = &self.selected {
                    let source = selected_source(app, self.selected_source_index);
                    let id = if source.source == "flatpak" { source.id.clone() } else { app.id.clone() };
                    let src = source.source.clone();
                    Task::perform(
                        async move { rakuos_packages::get_addons_for_app(&id, &src).unwrap_or_default() },
                        |addons| action::app(Message::AddonsLoaded(addons)),
                    )
                } else {
                    Task::none()
                }
            }
            Message::AddonsLoaded(addons) => {
                self.addon_list = addons;
                Task::none()
            }
            Message::ToggleAddonsDialog => {
                self.addon_dialog_open = !self.addon_dialog_open;
                Task::none()
            }
            Message::AddonOperation(kind, addon_id, addon_source) => {
                let pkg_name = self.addon_list.iter()
                    .find(|a| a["id"].as_str() == Some(addon_id.as_str()))
                    .and_then(|a| a["package_name"].as_str())
                    .unwrap_or(addon_id.as_str())
                    .to_string();
                let remote = if addon_source == "flatpak" { "flathub".to_string() } else { String::new() };
                let source = SourceOption {
                    id: addon_id.clone(),
                    source: addon_source,
                    package_name: pkg_name,
                    installed: false,
                    label: String::new(),
                    remote,
                    user_remote: false,
                    summary: String::new(),
                    description: String::new(),
                    version: String::new(),
                    developer: String::new(),
                    url_homepage: String::new(),
                    url_bugtracker: String::new(),
                    url_donation: String::new(),
                    url_help: String::new(),
                    url_faq: String::new(),
                    url_vcs_browser: String::new(),
                    url_contribute: String::new(),
                    license: String::new(),
                    screenshots: Vec::new(),
                    icon_path: String::new(),
                    icon_url: String::new(),
                };
                let app_id = self.selected.as_ref().map(|a| a.id.clone()).unwrap_or_default();
                self.operation_running = true;
                self.status.clear();
                Task::perform(
                    run_operation(app_id, source, kind),
                    |(ok, msg, _)| action::app(Message::AddonOperationFinished(ok, msg)),
                )
            }
            Message::AddonOperationFinished(_ok, msg) => {
                self.operation_running = false;
                self.status = msg;
                if let Some(app) = &self.selected {
                    let source = selected_source(app, self.selected_source_index);
                    let id = if source.source == "flatpak" { source.id.clone() } else { app.id.clone() };
                    let src = source.source.clone();
                    Task::perform(
                        async move { rakuos_packages::get_addons_for_app(&id, &src).unwrap_or_default() },
                        |addons| action::app(Message::AddonsLoaded(addons)),
                    )
                } else {
                    Task::none()
                }
            }
            Message::ScreenshotNext => {
                self.screenshot_index = self.screenshot_index.saturating_add(1);
                Task::none()
            }
            Message::ScreenshotPrev => {
                self.screenshot_index = self.screenshot_index.saturating_sub(1);
                Task::none()
            }
            Message::SelectScreenshot(index) => {
                self.screenshot_index = index;
                Task::none()
            }
            Message::ReviewsLoaded(reviews) => {
                self.reviews = reviews;
                self.reviews_loading = false;
                self.reviews_page = 0;
                Task::none()
            }
            Message::ReviewsPagePrev => {
                self.reviews_page = self.reviews_page.saturating_sub(1);
                Task::none()
            }
            Message::ReviewsPageNext => {
                self.reviews_page = self.reviews_page.saturating_add(1);
                Task::none()
            }
            Message::OpenReviewPopup => {
                self.review_popup_open = true;
                self.review_status.clear();
                Task::none()
            }
            Message::CloseReviewPopup => {
                self.review_popup_open = false;
                self.review_submit_running = false;
                self.review_status.clear();
                Task::none()
            }
            Message::ReviewRating(rating) => {
                self.review_rating = rating.clamp(1, 5);
                Task::none()
            }
            Message::ReviewNameChanged(value) => {
                self.review_name = value;
                Task::none()
            }
            Message::ReviewSummaryChanged(value) => {
                self.review_summary = value;
                Task::none()
            }
            Message::ReviewBodyChanged(value) => {
                self.review_body = value;
                Task::none()
            }
            Message::SubmitReview => {
                if self.review_summary.trim().is_empty() || self.review_body.trim().is_empty() {
                    self.review_status = "Summary and review text are required.".to_string();
                    return Task::none();
                }

                let Some(app) = self.selected.clone() else {
                    return Task::none();
                };
                let source = selected_source(&app, self.selected_source_index);
                let version = if source.version.is_empty() {
                    app.version
                } else {
                    source.version
                };
                let display = if self.review_name.trim().is_empty() {
                    None
                } else {
                    Some(self.review_name.clone())
                };
                self.review_submit_running = true;
                self.review_status.clear();
                submit_review(
                    app.id,
                    self.review_summary.clone(),
                    self.review_body.clone(),
                    self.review_rating * 20,
                    version,
                    display,
                )
            }
            Message::ReviewSubmitted(result) => {
                self.review_submit_running = false;
                match result {
                    Ok(()) => {
                        self.review_popup_open = false;
                        self.review_status.clear();
                        self.review_summary.clear();
                        self.review_body.clear();
                        self.review_name.clear();
                        self.review_rating = 4;
                        if let Some(app_id) = self
                            .selected
                            .as_ref()
                            .filter(|app| app_supports_reviews(app, self.selected_source_index))
                            .map(|app| app.id.clone())
                        {
                            self.start_reviews();
                            load_reviews(app_id)
                        } else {
                            Task::none()
                        }
                    }
                    Err(error) => {
                        self.review_status = error;
                        Task::none()
                    }
                }
            }
            Message::VoteReview(review_id, upvote) => {
                rakuos_reviews::vote_review(review_id, upvote);
                Task::none()
            }
            Message::AppImageUpdateSourceChanged(value) => {
                self.appimage_update_source = value;
                self.appimage_settings_status.clear();
                Task::none()
            }
            Message::AppImageUpdateUrlChanged(value) => {
                self.appimage_update_url = value;
                self.appimage_settings_status.clear();
                Task::none()
            }
            Message::AppImageUpdatePatternChanged(value) => {
                self.appimage_update_pattern = value;
                self.appimage_settings_status.clear();
                Task::none()
            }
            Message::SaveAppImageSettings => {
                let Some(app) = self.selected.as_ref() else {
                    return Task::none();
                };
                if app.source != "appimage" {
                    return Task::none();
                }
                save_appimage_settings(
                    app.id.clone(),
                    self.appimage_update_source.clone(),
                    self.appimage_update_url.clone(),
                    self.appimage_update_pattern.clone(),
                )
            }
            Message::AppImageSettingsSaved(ok, status) => {
                self.appimage_settings_status = status;
                if ok {
                    if let Some(app) = self.selected.as_mut() {
                        if app.source == "appimage" {
                            app.update_source = self.appimage_update_source.clone();
                            app.update_url = self.appimage_update_url.clone();
                            app.update_pattern = self.appimage_update_pattern.clone();
                        }
                    }
                }
                Task::none()
            }
            Message::SystemLoaded(data) => {
                self.loading = false;
                self.system_loading = false;
                self.system_switch_de = current_de_index(&data.status.image);
                self.system_switch_branch = current_branch_index(&data.status.image);
                self.system_data = Some(data);
                Task::none()
            }
            Message::SystemAction(action) => {
                self.system_loading = true;
                self.system_status = format!("Running {action}...");
                self.system_success = false;
                self.system_dialog = None;
                run_system_action(action)
            }
            Message::SystemActionFinished(ok, status) => {
                self.system_loading = false;
                self.system_status = status;
                self.system_success = ok;
                if ok {
                    self.system_reboot_required = true;
                }
                if ok {
                    load_system_page()
                } else {
                    Task::none()
                }
            }
            Message::OpenSystemDialog(dialog) => {
                self.system_dialog = Some(dialog);
                Task::none()
            }
            Message::CloseSystemDialog => {
                self.system_dialog = None;
                Task::none()
            }
            Message::SelectSystemDe(index) => {
                self.system_switch_de = index.min(DESKTOP_IMAGES.len().saturating_sub(1));
                Task::none()
            }
            Message::SelectSystemBranch(index) => {
                self.system_switch_branch = index.min(IMAGE_BRANCHES.len().saturating_sub(1));
                Task::none()
            }
            Message::PrepareSwitchImage(label, repo, tag) => {
                self.system_switch_label = label;
                self.system_switch_repo = repo;
                self.system_switch_tag = tag;
                self.system_dialog = Some(SystemDialog::SwitchConfirm);
                Task::none()
            }
            Message::SwitchImage => {
                let repo = self.system_switch_repo.clone();
                let tag = self.system_switch_tag.clone();
                self.system_dialog = Some(SystemDialog::SwitchProgress);
                self.system_switch_running = true;
                self.system_switch_success = false;
                self.system_switch_log.clear();
                run_switch_image(repo, tag)
            }
            Message::SwitchImageFinished(ok, log) => {
                self.system_switch_running = false;
                self.system_switch_success = ok;
                self.system_switch_log = log;
                self.system_status = if ok {
                    "Switch staged - reboot to apply.".to_string()
                } else {
                    "Image switch failed. Check the dialog log.".to_string()
                };
                self.system_success = ok;
                self.system_reboot_required = ok;
                Task::none()
            }
            Message::OpenKernelArgsDialog => {
                self.kargs_cmdline = rakuos_updates::get_current_cmdline();
                self.kargs_user = rakuos_updates::get_user_kernel_args();
                self.kargs_input = self.kargs_user.clone();
                self.kargs_running = false;
                self.kargs_status.clear();
                self.kargs_success = false;
                self.system_dialog = Some(SystemDialog::KernelArgs);
                Task::none()
            }
            Message::KernelArgsInputChanged(input) => {
                self.kargs_input = input;
                Task::none()
            }
            Message::KernelArgsClear => {
                self.kargs_input.clear();
                Task::none()
            }
            Message::ApplyKernelArgs => {
                let options = self.kargs_input.trim().to_string();
                self.kargs_running = true;
                self.kargs_status = "Applying…".to_string();
                self.kargs_success = false;
                run_set_kargs(options)
            }
            Message::KernelArgsFinished(ok, msg) => {
                self.kargs_running = false;
                self.kargs_status = msg;
                self.kargs_success = ok;
                if ok {
                    self.kargs_user = self.kargs_input.trim().to_string();
                    self.system_reboot_required = true;
                }
                Task::none()
            }
            Message::RebootSystem => {
                let (ok, msg) = rakuos_updates::schedule_reboot();
                self.system_status = if ok {
                    "Reboot requested.".to_string()
                } else {
                    format!("Reboot failed: {msg}")
                };
                Task::none()
            }
            Message::SelectSettingsTab(tab) => {
                self.settings_tab = tab;
                match tab {
                    SettingsTab::Updates => Task::none(),
                    SettingsTab::Flatpak => load_flatpak_remotes(),
                    SettingsTab::Firmware => load_firmware_data(),
                }
            }
            Message::SettingsLoaded(settings) => {
                self.settings_data = settings;
                Task::none()
            }
            Message::SettingsInterval(value) => {
                self.settings_data.update_interval = value;
                self.settings_saved = false;
                Task::none()
            }
            Message::SettingsTogglePackages(value) => {
                self.settings_data.auto_check_packages = value;
                self.settings_saved = false;
                Task::none()
            }
            Message::SettingsToggleFlatpak(value) => {
                self.settings_data.auto_check_flatpak = value;
                self.settings_saved = false;
                Task::none()
            }
            Message::SettingsToggleImage(value) => {
                self.settings_data.auto_check_image = value;
                self.settings_saved = false;
                Task::none()
            }
            Message::SettingsToggleAppImages(value) => {
                self.settings_data.auto_check_appimages = value;
                self.settings_saved = false;
                Task::none()
            }
            Message::SettingsToggleAutoUpdate(value) => {
                self.settings_data.auto_update = value;
                self.settings_saved = false;
                Task::none()
            }
            Message::SettingsToggleBootcAutoUpdate(value) => {
                let (ok, _) = rakuos_updates::set_bootc_autoupdate(value);
                if ok { self.bootc_autoupdate = value; }
                Task::none()
            }
            Message::SaveSettings => {
                save_settings(&self.settings_data);
                self.settings_saved = true;
                Task::none()
            }
            Message::FlatpakRemotesLoaded(remotes, has_flathub) => {
                self.flatpak_remotes = remotes;
                self.flatpak_has_flathub = has_flathub;
                Task::none()
            }
            Message::AddFlathub => run_flatpak_action("add-flathub".to_string(), String::new(), true, true),
            Message::OpenFlatpakAddRemote => {
                self.flatpak_add_remote_open = true;
                self.flatpak_status.clear();
                Task::none()
            }
            Message::CloseFlatpakAddRemote => {
                self.flatpak_add_remote_open = false;
                Task::none()
            }
            Message::FlatpakNewNameChanged(value) => {
                self.flatpak_new_name = value;
                Task::none()
            }
            Message::FlatpakNewUrlChanged(value) => {
                self.flatpak_new_url = value;
                Task::none()
            }
            Message::FlatpakNewSystemChanged(value) => {
                self.flatpak_new_system = value;
                Task::none()
            }
            Message::AddFlatpakRemote => {
                let name = self.flatpak_new_name.trim().to_string();
                let url = self.flatpak_new_url.trim().to_string();
                if name.is_empty() || url.is_empty() {
                    self.flatpak_status = "Name and URL are required.".to_string();
                    return Task::none();
                }
                let system = self.flatpak_new_system;
                self.flatpak_add_remote_open = false;
                run_flatpak_add_remote(name, url, system)
            }
            Message::ToggleFlatpakRemote(name, enabled, system) => {
                run_flatpak_action("toggle".to_string(), name, enabled, system)
            }
            Message::RemoveFlatpakRemote(name, system) => {
                run_flatpak_action("remove".to_string(), name, false, system)
            }
            Message::FlatpakActionFinished(ok, status) => {
                self.flatpak_status = status;
                if ok {
                    self.flatpak_new_name.clear();
                    self.flatpak_new_url.clear();
                    load_flatpak_remotes()
                } else { Task::none() }
            }
            Message::FirmwareLoaded(data) => {
                self.firmware_data = data;
                Task::none()
            }
            Message::ToggleFirmwareRemote(id, enabled) => run_firmware_toggle(id, enabled),
            Message::RefreshFirmware => {
                self.firmware_refreshing = true;
                self.firmware_status.clear();
                run_firmware_refresh()
            }
            Message::FirmwareActionFinished(ok, status) => {
                self.firmware_refreshing = false;
                self.firmware_status = status;
                if ok { load_firmware_data() } else { Task::none() }
            }
            Message::PageScrolled(viewport) => {
                let page = self.active_page();
                if (page.is_subcategory() || page.is_top_category()) && self.page_visible_count < self.page_apps.len() {
                    let relative = viewport.relative_offset();
                    if relative.y.is_finite() && relative.y > 0.82 {
                        self.page_visible_count = (self.page_visible_count + CATEGORY_PAGE_SIZE).min(self.page_apps.len());
                    }
                } else if page == NavPage::WebApps && self.webapp_visible_count < self.webapp_catalog.len() {
                    let relative = viewport.relative_offset();
                    if relative.y.is_finite() && relative.y > 0.82 {
                        self.webapp_visible_count = (self.webapp_visible_count + WEBAPP_PAGE_SIZE).min(self.webapp_catalog.len());
                    }
                }
                Task::none()
            }
            Message::LoadMoreApps => {
                if self.active_page() == NavPage::WebApps {
                    self.webapp_visible_count = (self.webapp_visible_count + WEBAPP_PAGE_SIZE).min(self.webapp_catalog.len());
                } else {
                    self.page_visible_count = (self.page_visible_count + CATEGORY_PAGE_SIZE).min(self.page_apps.len());
                }
                Task::none()
            }
            Message::OpenCustomWebApp => {
                self.webapp_custom_open = true;
                self.webapp_custom_status.clear();
                Task::none()
            }
            Message::CloseCustomWebApp => {
                self.webapp_custom_open = false;
                self.webapp_custom_running = false;
                self.webapp_custom_status.clear();
                Task::none()
            }
            Message::CustomWebAppNameChanged(value) => {
                self.webapp_custom_name = value;
                Task::none()
            }
            Message::CustomWebAppUrlChanged(value) => {
                self.webapp_custom_url = value;
                Task::none()
            }
            Message::CustomWebAppDescriptionChanged(value) => {
                self.webapp_custom_description = value;
                Task::none()
            }
            Message::CustomWebAppCategoryChanged(value) => {
                self.webapp_custom_category = value;
                Task::none()
            }
            Message::CustomWebAppIconSourceChanged(value) => {
                self.webapp_custom_icon_source = value;
                Task::none()
            }
            Message::BrowseCustomWebAppIcon => pick_custom_webapp_icon(),
            Message::CustomWebAppIconSelected(result) => {
                match result {
                    Ok(path) if !path.trim().is_empty() => {
                        self.webapp_custom_icon_source = path;
                        self.webapp_custom_status.clear();
                    }
                    Ok(_) => {}
                    Err(error) => {
                        self.webapp_custom_status = error;
                    }
                }
                Task::none()
            }
            Message::AddCustomWebApp => {
                if self.webapp_custom_name.trim().is_empty() || self.webapp_custom_url.trim().is_empty() {
                    self.webapp_custom_status = "Name and URL are required.".to_string();
                    return Task::none();
                }
                self.webapp_custom_running = true;
                self.webapp_custom_status = "Adding web app...".to_string();
                add_custom_webapp(
                    self.webapp_custom_name.trim().to_string(),
                    self.webapp_custom_url.trim().to_string(),
                    self.webapp_custom_description.trim().to_string(),
                    self.webapp_custom_category.trim().to_string(),
                    self.webapp_custom_icon_source.trim().to_string(),
                )
            }
            Message::CustomWebAppFinished(ok, status) => {
                self.webapp_custom_running = false;
                self.webapp_custom_status = status;
                if ok {
                    self.webapp_custom_open = false;
                    self.webapp_custom_name.clear();
                    self.webapp_custom_url.clear();
                    self.webapp_custom_description.clear();
                    self.webapp_custom_category = "Network".to_string();
                    self.webapp_custom_icon_source.clear();
                    self.loading = true;
                    load_apps(LoadKind::WebApps)
                } else {
                    Task::none()
                }
            }
            Message::SelectInstalledTab(tab) => {
                self.installed_tab = tab;
                Task::none()
            }
            Message::Operation(kind) => {
                let Some(app) = self.selected.clone() else {
                    return Task::none();
                };
                let source = selected_source(&app, self.selected_source_index);

                // Queue if another op is already running.
                if self.operation_running {
                    self.install_queue.push_back((
                        app.name.clone(),
                        app.id.clone(),
                        source,
                        kind,
                        app.icon_path.clone(),
                        app.icon_url.clone(),
                    ));
                    return Task::none();
                }

                self.operation_running = true;
                self.active_op_name = app.name.clone();
                self.active_op_is_remove = matches!(kind, OperationKind::Remove);
                self.active_op_progress = 0.02;
                self.active_op_icon_path = app.icon_path.clone();
                self.active_op_icon_url = app.icon_url.clone();
                self.status.clear();
                Task::perform(run_operation(app.id.clone(), source, kind), |result| {
                    let (ok, msg, app) = result;
                    action::app(Message::OperationFinished(ok, msg, app))
                })
            }
            Message::OperationFinished(ok, msg, app) => {
                self.operation_running = false;
                self.status = msg;
                if ok {
                    if let Some(app) = app {
                        self.selected_source_index = preferred_source_index(&app);
                        self.selected = Some(app);
                    }
                }
                // Start next queued operation if any.
                if let Some((display_name, id, source, kind, icon_path, icon_url)) = self.install_queue.pop_front() {
                    self.operation_running = true;
                    self.active_op_name = display_name;
                    self.active_op_is_remove = matches!(kind, OperationKind::Remove);
                    self.active_op_progress = 0.02;
                    self.active_op_icon_path = icon_path;
                    self.active_op_icon_url = icon_url;
                    self.status.clear();
                    return Task::perform(run_operation(id, source, kind), |result| {
                        let (ok, msg, app) = result;
                        action::app(Message::OperationFinished(ok, msg, app))
                    });
                }
                self.active_op_name.clear();
                self.active_op_icon_path.clear();
                self.active_op_icon_url.clear();
                Task::none()
            }
            Message::UpdatesCacheLoaded(data) => {
                self.updates_loading = false;
                self.updates_data = data;
                self.updates_cache_mtime = std::fs::metadata(daemon_cache_path())
                    .and_then(|m| m.modified())
                    .ok();
                if self.updates_data.is_none() {
                    self.updates_status = "No cached update check yet. Use Check for updates.".to_string();
                } else {
                    self.updates_status.clear();
                }
                // Check whether a bootc op survived a software center restart
                Task::perform(
                    tokio::task::spawn_blocking(rakuos_updates::is_bootc_op_active),
                    |res| action::app(Message::BootcReconnect(res.unwrap_or(false))),
                )
            }
            Message::BootcReconnect(active) => {
                if active && !self.updates_updating {
                    self.updates_updating = true;
                    self.updates_active = Some(UpdateTarget::Image);
                    self.updates_status = "System update in progress — reconnected…".to_string();
                    self.updates_image_progress = 0.02;
                    self.updates_image_progress_arc.store(2, Ordering::Relaxed);
                    let progress_arc = Arc::clone(&self.updates_image_progress_arc);
                    run_update_target(UpdateTarget::Image, self.updates_data.clone(), progress_arc)
                } else {
                    Task::none()
                }
            }
            Message::CheckUpdates => {
                self.updates_loading = true;
                self.updates_checking = true;
                self.updates_updating = false;
                self.updates_active = None;
                self.updates_completed.clear();
                self.updates_image_done = false;
                self.updates_status = "Checking for updates...".to_string();
                check_updates()
            }
            Message::UpdatesChecked(result) => {
                self.updates_loading = false;
                self.updates_checking = false;
                match result {
                    Ok(data) => {
                        write_updates_cache(&data);
                        self.updates_cache_mtime = std::fs::metadata(daemon_cache_path())
                            .and_then(|m| m.modified())
                            .ok();
                        write_badge_count(data.total);
                        self.updates_status = if data.total == 0 && !data.reboot_required {
                            "System is up to date.".to_string()
                        } else {
                            String::new()
                        };
                        self.updates_data = Some(data);
                    }
                    Err(error) => {
                        self.updates_status = format!("Update check failed: {error}");
                    }
                }
                Task::none()
            }
            Message::UpdateTarget(target) => {
                if self.updates_updating {
                    return Task::none();
                }
                self.updates_updating = true;
                self.updates_active = Some(target.clone());
                self.updates_status = update_target_status(&target);
                self.updates_image_progress = 0.02;
                self.updates_image_progress_arc.store(2, Ordering::Relaxed);
                let progress_arc = Arc::clone(&self.updates_image_progress_arc);
                run_update_target(target, self.updates_data.clone(), progress_arc)
            }
            Message::UpdateFinished(target, ok, status) => {
                self.updates_updating = false;
                self.updates_active = None;
                self.updates_status = status;

                if ok {
                    mark_update_complete(
                        &mut self.updates_completed,
                        &mut self.updates_image_done,
                        &target,
                        self.updates_data.as_ref(),
                    );
                    let remaining = remaining_update_count(
                        self.updates_data.as_ref(),
                        &self.updates_completed,
                        self.updates_image_done,
                    );
                    write_badge_count(remaining);
                    if remaining == 0 {
                        self.updates_status = if self.updates_image_done
                            || self
                                .updates_data
                                .as_ref()
                                .map(|data| data.reboot_required)
                                .unwrap_or(false)
                        {
                            "Updates applied. Reboot to finish.".to_string()
                        } else {
                            "All updates complete.".to_string()
                        };
                    }
                }
                Task::none()
            }
            Message::DaemonTick => {
                self.overlay_update_running =
                    std::path::Path::new("/run/rakuos-overlay.lock").exists();

                if consume_quit_request() {
                    let _ = std::fs::remove_file(pid_file());
                    std::process::exit(0);
                }

                let mut tasks = Vec::new();
                if consume_show_request() {
                    if let Some(window_id) = self.tray_window_id {
                        tasks.push(window::minimize(window_id, false));
                        tasks.push(window::set_mode(window_id, window::Mode::Windowed));
                        tasks.push(window::gain_focus(window_id));
                    } else if self.tray_mode {
                        // libcosmic may have already created a window despite
                        // no_main_window(true) — reuse it instead of opening a second one.
                        if let Some(existing_id) = self.core.main_window_id() {
                            self.tray_window_id = Some(existing_id);
                            tasks.push(window::minimize(existing_id, false));
                            tasks.push(window::set_mode(existing_id, window::Mode::Windowed));
                            tasks.push(window::gain_focus(existing_id));
                        } else {
                            let (id, open_task) = window::open(window::Settings {
                                size: Size::new(1000.0, 700.0),
                                min_size: Some(Size::new(800.0, 600.0)),
                                decorations: false,
                                exit_on_close_request: false,
                                ..window::Settings::default()
                            });
                            self.tray_window_id = Some(id);
                            self.core_mut().set_main_window_id(Some(id));
                            tasks.push(open_task.discard());
                        }
                    } else if let Some(window_id) = self.core.main_window_id() {
                        tasks.push(window::minimize(window_id, false));
                        tasks.push(window::set_mode(window_id, window::Mode::Windowed));
                        tasks.push(window::gain_focus(window_id));
                    }
                }

                if let Some(path) = consume_open_file_request() {
                    if let Some(kind) = local_file_kind(&path) {
                        self.local_file = Some(LocalFileState {
                            path: path.clone(),
                            kind: kind.to_string(),
                            info: None,
                            loading: true,
                            installing: false,
                            success: false,
                            status: String::new(),
                            log: String::new(),
                        });
                        self.selected = None;
                        self.selected_source_index = 0;
                        self.source_menu_open = false;
                        self.screenshot_index = 0;
                        self.clear_reviews();
                        self.loading = true;
                        tasks.push(load_local_file(path, kind.to_string()));
                    }
                }

                if self.updates_updating {
                    let raw = self.updates_image_progress_arc.load(Ordering::Relaxed);
                    self.updates_image_progress = raw as f32 / 100.0;
                }

                if consume_check_trigger() && !self.updates_checking && !self.updates_updating {
                    self.updates_checking = true;
                    if self.active_page() == NavPage::Updates {
                        self.updates_loading = true;
                        self.updates_status = "Checking for updates...".to_string();
                    }
                    tasks.push(check_updates());
                }

                // Mirror GTK's daemon-cache mtime watcher: if the daemon wrote the cache
                // directly (e.g. UI PID wasn't detected), reload the Updates page data.
                if !self.updates_checking && !self.updates_updating {
                    let current_mtime = std::fs::metadata(daemon_cache_path())
                        .and_then(|m| m.modified())
                        .ok();
                    if current_mtime.is_some() && current_mtime != self.updates_cache_mtime {
                        self.updates_cache_mtime = current_mtime;
                        tasks.push(load_updates_cache());
                    }
                }

                Task::batch(tasks)
            }
            Message::WindowCloseRequested(id) => {
                if self.tray_mode {
                    self.tray_window_id = None;
                    self.core_mut().set_main_window_id(None);
                    window::close(id)
                } else {
                    window::minimize(id, true)
                }
            }
            Message::LaunchUrl(url) => {
                let _ = std::process::Command::new("xdg-open").arg(url).spawn();
                Task::none()
            }
            Message::LaunchWebApp(id) => {
                let _ = std::process::Command::new("gtk-launch")
                    .arg(format!("rakuos-webapp-{}", id))
                    .spawn();
                Task::none()
            }
        }
    }

    fn view(&self) -> Element<'_, Message> {
        let content: Element<_> = widget::responsive(move |mut size| {
            size.width = size.width.min(MAX_GRID_WIDTH);
            widget::id_container(
                widget::scrollable(
                    widget::container(
                        widget::container(self.view_responsive(size)).max_width(MAX_GRID_WIDTH),
                    )
                    .align_x(Alignment::Center),
                )
                .on_scroll(Message::PageScrolled),
                self.scrollable_id.clone(),
            )
            .into()
        })
        .into();

        let popup = if self.addon_dialog_open && !self.addon_list.is_empty() {
            Some(pages::addons_popup(self))
        } else if self.review_popup_open {
            Some(pages::review_popup(self))
        } else if self.system_dialog.is_some() {
            Some(pages::system_popup(self))
        } else if self.flatpak_add_remote_open {
            Some(pages::flatpak_add_remote_popup(self))
        } else if self.webapp_custom_open {
            Some(pages::custom_webapp_popup(self))
        } else {
            None
        };

        let main: Element<_> = if let Some(popup) = popup {
            let overlay: Element<_> = widget::container(popup)
                .width(Length::Fill)
                .height(Length::Fill)
                .center_x(Length::Fill)
                .center_y(Length::Fill)
                .into();
            cosmic::iced::widget::stack(vec![content, overlay]).into()
        } else {
            content
        };

        // ── Install queue banner — full-width bottom bar with real progress ──
        if self.operation_running || !self.install_queue.is_empty() {
            let verb = if self.active_op_is_remove { "Removing" } else { "Installing" };

            let icon_el = pages::common::app_icon(&self.active_op_icon_path, &self.active_op_icon_url, 24);

            let mut label_row = widget::row()
                .push(icon_el)
                .push(
                    widget::text::body(format!("{} {}", verb, self.active_op_name))
                        .width(Length::Fill),
                )
                .spacing(10)
                .align_y(cosmic::iced::Alignment::Center);
            if !self.install_queue.is_empty() {
                label_row = label_row.push(
                    widget::text::caption(format!("{} more in queue", self.install_queue.len())),
                );
            }

            let progress = self.active_op_progress.max(0.02).min(1.0);
            let bar: Element<'_, Message> =
                widget::progress_bar(0.0_f32..=1.0_f32, progress).into();

            let banner: Element<'_, Message> = widget::container(
                widget::column()
                    .push(label_row)
                    .push(bar)
                    .spacing(4),
            )
            .padding([8, 16])
            .width(Length::Fill)
            .class(cosmic::theme::Container::Secondary)
            .into();

            let main_el: Element<'_, Message> = main;
            widget::column()
                .push(main_el)
                .push(banner)
                .into()
        } else {
            main
        }
    }
}

impl SoftwareApp {
    fn active_page(&self) -> NavPage {
        self.nav_model
            .active_data::<NavPage>()
            .copied()
            .unwrap_or_default()
    }

    fn clear_reviews(&mut self) {
        self.reviews.clear();
        self.reviews_loading = false;
        self.reviews_page = 0;
        self.review_popup_open = false;
        self.review_submit_running = false;
        self.review_status.clear();
    }

    fn start_reviews(&mut self) {
        self.reviews.clear();
        self.reviews_loading = true;
        self.reviews_page = 0;
        self.review_popup_open = false;
        self.review_submit_running = false;
        self.review_status.clear();
    }

    fn load_appimage_settings_from_app(&mut self, app: &NativeApp) {
        self.appimage_settings_status.clear();
        if app.source == "appimage" {
            self.appimage_update_source = if app.update_source.is_empty() {
                "none".to_string()
            } else {
                app.update_source.clone()
            };
            self.appimage_update_url = app.update_url.clone();
            self.appimage_update_pattern = app.update_pattern.clone();
        }
    }
}

fn build_nav_model(
    expanded_category: Option<&'static str>,
    active_page: NavPage,
) -> widget::nav_bar::Model {
    let mut nav_model = widget::nav_bar::Model::default();
    insert_nav_page(&mut nav_model, NavPage::Home, active_page);

    for page in [
        NavPage::Installed,
        NavPage::Updates,
        NavPage::WebApps,
        NavPage::Settings,
    ] {
        insert_nav_page(&mut nav_model, page, active_page);
    }
    if !is_live_environment() {
        insert_nav_page(&mut nav_model, NavPage::System, active_page);
    }

    for (index, group) in CATEGORY_TREE.iter().enumerate() {
        let top_page = NavPage::Category {
            title: group.title,
            cat: group.cat,
            icon: group.icon,
            sub: false,
        };
        if index == 0 {
            insert_nav_page_with_divider(&mut nav_model, top_page, active_page);
        } else {
            insert_nav_page(&mut nav_model, top_page, active_page);
        }

        if expanded_category == Some(group.cat) {
            for sub in group.subs {
                let sub_page = NavPage::Category {
                    title: sub.title,
                    cat: sub.cat,
                    icon: group.icon,
                    sub: true,
                };
                let id = nav_model
                    .insert()
                    .icon(sub_page.icon())
                    .text(format!("  {}", sub_page.title()))
                    .data::<NavPage>(sub_page)
                    .id();
                if sub_page == active_page {
                    nav_model.activate(id);
                }
            }
        }
    }

    nav_model
}

fn is_live_environment() -> bool {
    std::env::var("USER").unwrap_or_default() == "liveuser" || PathBuf::from("/run/live").exists()
}

fn insert_nav_page(
    nav_model: &mut widget::nav_bar::Model,
    page: NavPage,
    active_page: NavPage,
) {
    let id = nav_model
        .insert()
        .icon(page.icon())
        .text(page.title())
        .data::<NavPage>(page)
        .id();
    if page == active_page {
        nav_model.activate(id);
    }
}

fn insert_nav_page_with_divider(
    nav_model: &mut widget::nav_bar::Model,
    page: NavPage,
    active_page: NavPage,
) {
    let id = nav_model
        .insert()
        .divider_above(true)
        .icon(page.icon())
        .text(page.title())
        .data::<NavPage>(page)
        .id();
    if page == active_page {
        nav_model.activate(id);
    }
}

fn parent_category_for(cat: &str) -> Option<&'static str> {
    for group in CATEGORY_TREE {
        if group.cat == cat || group.subs.iter().any(|sub| sub.cat == cat) {
            return Some(group.cat);
        }
    }
    None
}

fn normalize_file_arg(arg: &str) -> String {
    arg.strip_prefix("file://").unwrap_or(arg).to_string()
}

fn local_file_kind(path: &str) -> Option<&'static str> {
    let lower = path.to_lowercase();
    if lower.ends_with(".rpm") {
        Some("rpm")
    } else if lower.ends_with(".flatpak") {
        Some("flatpak")
    } else if lower.ends_with(".flatpakref") {
        Some("flatpakref")
    } else if lower.ends_with(".appimage") {
        Some("appimage")
    } else {
        None
    }
}

pub(crate) fn local_file_action_label(action: &str) -> &'static str {
    match action {
        "reinstall" => "Reinstall",
        "upgrade" => "Upgrade",
        _ => "Install",
    }
}

fn load_local_file(path: String, kind: String) -> Task<Message> {
    Task::perform(
        async move {
            let info = match kind.as_str() {
                "rpm" => rakuos_packages::get_local_rpm_info(&path),
                "flatpak" => rakuos_flatpak::get_local_flatpak_info(&path),
                "flatpakref" => rakuos_flatpak::get_flatpakref_info(&path),
                "appimage" => rakuos_appimages::get_appimage_info_for_display(&path),
                _ => return Message::LocalFileFailed("Unsupported file type.".to_string()),
            };
            Message::LocalFileLoaded(info)
        },
        |message| action::app(message),
    )
}

fn install_local_file(path: String, kind: String, action_name: String, package_name: String) -> Task<Message> {
    Task::perform(
        async move {
            let mut log = String::new();
            let mut ok = false;

            match kind.as_str() {
                "rpm" => {
                    let iter: Box<dyn Iterator<Item = String> + Send> = if action_name == "reinstall" {
                        Box::new(rakuos_packages::reinstall_local_rpm_stream(&package_name, &path))
                    } else {
                        Box::new(rakuos_packages::install_local_rpm_stream(&path))
                    };
                    for line in iter {
                        if let Some(code) = line.strip_prefix("__done__") {
                            ok = code.trim() == "0";
                        } else if !line.trim().is_empty() {
                            log.push_str(&line);
                            log.push('\n');
                        }
                    }
                }
                "flatpak" => {
                    for line in rakuos_flatpak::install_local_bundle_stream(&path) {
                        if let Some(code) = line.strip_prefix("__done__") {
                            ok = code.trim() == "0";
                        } else if !line.trim().is_empty() {
                            log.push_str(&line);
                            log.push('\n');
                        }
                    }
                }
                "flatpakref" => {
                    for line in rakuos_flatpak::install_flatpakref_stream(&path) {
                        if let Some(code) = line.strip_prefix("__done__") {
                            ok = code.trim() == "0";
                        } else if !line.trim().is_empty() {
                            log.push_str(&line);
                            log.push('\n');
                        }
                    }
                }
                "appimage" => {
                    let (success, message, _) = rakuos_appimages::install_appimage(&path);
                    ok = success;
                    log = message;
                }
                _ => {
                    log = "Unsupported file type.".to_string();
                }
            }

            (ok, log)
        },
        |(ok, log)| action::app(Message::LocalFileInstallFinished(ok, log)),
    )
}

fn load_home() -> Task<Message> {
    Task::perform(
        async {
            let (picks, popular, updated, new) = rakuos_home::load_all().await;
            vec![
                (HomeSection::Picks, picks),
                (HomeSection::Popular, popular),
                (HomeSection::Updated, updated),
                (HomeSection::New, new),
            ]
        },
        |sections| action::app(Message::HomeLoaded(sections)),
    )
}

fn load_installed_page() -> Task<Message> {
    Task::perform(async { load_installed_data() }, |data| {
        action::app(Message::InstalledLoaded(data))
    })
}

fn load_apps(kind: LoadKind) -> Task<Message> {
    Task::perform(
        async move {
            let loaded = match &kind {
                LoadKind::Home => Ok(Vec::new()),
                LoadKind::Category(page) => {
                    let categories = page.category_ids();
                    load_categories(&categories)
                }
                LoadKind::Installed => load_installed(),
                LoadKind::Search(query) => rakuos_packages::search(query),
                LoadKind::WebApps => Ok(load_webapp_catalog()),
                LoadKind::WebAppSearch(query) => Ok(load_webapp_search(query)),
            };
            match loaded {
                Ok(apps) => Message::Loaded(kind, apps),
                Err(err) => Message::LoadFailed(err.to_string()),
            }
        },
        |message| action::app(message),
    )
}

fn load_system_page() -> Task<Message> {
    Task::perform(
        async {
            SystemPageData {
                status: rakuos_updates::get_system_status(),
                overlay: rakuos_updates::get_overlay_status(),
            }
        },
        |data| action::app(Message::SystemLoaded(data)),
    )
}

fn run_system_action(action: String) -> Task<Message> {
    Task::perform(
        async move {
            let mut exit_code = 0;
            let mut lines = Vec::new();
            let stream: Box<dyn Iterator<Item = String> + Send> = match action.as_str() {
                "upgrade" => Box::new(rakuos_updates::pkexec_upgrade_stream()),
                "rollback" => Box::new(rakuos_updates::rollback_stream()),
                "soft-reset" => Box::new(rakuos_updates::reset_overlay_stream("soft")),
                "full-reset" => Box::new(rakuos_updates::reset_overlay_stream("full")),
                _ => return (false, format!("Unknown system action: {action}")),
            };
            for line in stream {
                if let Some(code) = line.strip_prefix("__done__") {
                    exit_code = code.trim().parse().unwrap_or(1);
                } else if !line.trim().is_empty() {
                    lines.push(line);
                }
            }
            let ok = exit_code == 0;
            let tail = lines.into_iter().rev().take(6).collect::<Vec<_>>();
            let mut status = if ok {
                format!("{action} completed.")
            } else {
                format!("{action} failed.")
            };
            if !tail.is_empty() {
                status.push('\n');
                status.push_str(&tail.into_iter().rev().collect::<Vec<_>>().join("\n"));
            }
            (ok, status)
        },
        |(ok, status)| action::app(Message::SystemActionFinished(ok, status)),
    )
}

fn run_set_kargs(options: String) -> Task<Message> {
    Task::perform(
        async move {
            let mut exit_code = 1i32;
            let mut last_line = String::new();
            for line in rakuos_updates::set_kernel_args_stream(&options) {
                if let Some(code) = line.strip_prefix("__done__") {
                    exit_code = code.trim().parse().unwrap_or(1);
                } else if !line.trim().is_empty() {
                    last_line = line;
                }
            }
            let ok = exit_code == 0;
            let msg = if ok {
                if options.trim().is_empty() {
                    "Kernel args cleared — reboot to apply.".to_string()
                } else {
                    "Kernel args staged — reboot to apply.".to_string()
                }
            } else {
                if last_line.is_empty() {
                    "Failed to apply kernel args.".to_string()
                } else {
                    last_line
                }
            };
            (ok, msg)
        },
        |(ok, msg)| action::app(Message::KernelArgsFinished(ok, msg)),
    )
}

fn run_switch_image(repo: String, tag: String) -> Task<Message> {
    Task::perform(
        async move {
            let target = format!("{repo}:{tag}");
            let mut exit_code = 0;
            let mut lines = Vec::new();
            for line in rakuos_updates::pkexec_reset_overlay_and_switch_stream(&target) {
                if let Some(code) = line.strip_prefix("__done__") {
                    exit_code = code.trim().parse().unwrap_or(1);
                } else if !line.trim().is_empty() {
                    lines.push(line);
                }
            }
            (exit_code == 0, lines.join("\n"))
        },
        |(ok, log)| action::app(Message::SwitchImageFinished(ok, log)),
    )
}

fn parse_image_ref(image_ref: &str) -> (String, String, bool) {
    let (image_path, tag) = image_ref.rsplit_once(':').unwrap_or((image_ref, ""));
    let path_without_registry = image_path
        .find('/')
        .map(|index| &image_path[index + 1..])
        .unwrap_or(image_path);
    let normalized = path_without_registry
        .split_once("rakuos/")
        .map(|(_, rest)| rest)
        .unwrap_or(path_without_registry);
    let is_nvidia = normalized.ends_with("/nvidia") || normalized.ends_with("-nvidia");
    let name = if let Some(without_nvidia) = normalized.strip_suffix("/nvidia") {
        without_nvidia.rsplit('/').next().unwrap_or("").to_string()
    } else {
        normalized
            .rsplit('/')
            .next()
            .unwrap_or("")
            .trim_end_matches("-nvidia")
            .to_string()
    };
    (name, tag.to_string(), is_nvidia)
}

fn current_de_index(image_ref: &str) -> usize {
    let (name, _, _) = parse_image_ref(image_ref);
    DESKTOP_IMAGES
        .iter()
        .position(|image| image.image_name == name)
        .unwrap_or(0)
}

fn current_branch_index(image_ref: &str) -> usize {
    let (_, tag, _) = parse_image_ref(image_ref);
    let label = if tag.starts_with("staging") {
        "Staging"
    } else if tag.starts_with("rawhide") {
        "Rawhide"
    } else {
        "Stable"
    };
    IMAGE_BRANCHES
        .iter()
        .position(|branch| branch.label == label)
        .unwrap_or(0)
}

fn load_settings() -> Task<Message> {
    Task::perform(async { read_settings() }, |settings| {
        action::app(Message::SettingsLoaded(settings))
    })
}

fn read_settings() -> SettingsData {
    std::fs::read_to_string(settings_path())
        .ok()
        .and_then(|text| serde_json::from_str(&text).ok())
        .unwrap_or_default()
}

fn save_settings(settings: &SettingsData) {
    let path = settings_path();
    if let Some(parent) = path.parent() {
        let _ = std::fs::create_dir_all(parent);
    }
    if let Ok(json) = serde_json::to_string_pretty(settings) {
        let _ = std::fs::write(path, json);
    }
}

fn settings_path() -> PathBuf {
    std::env::var_os("HOME")
        .map(PathBuf::from)
        .unwrap_or_else(|| PathBuf::from("/tmp"))
        .join(".config/rakuos/software-settings.json")
}

fn load_flatpak_remotes() -> Task<Message> {
    Task::perform(
        async {
            (
                rakuos_flatpak::get_remotes(),
                rakuos_flatpak::has_flathub(),
            )
        },
        |(remotes, has_flathub)| action::app(Message::FlatpakRemotesLoaded(remotes, has_flathub)),
    )
}

fn run_flatpak_action(action_name: String, name: String, enabled: bool, system: bool) -> Task<Message> {
    Task::perform(
        async move {
            match action_name.as_str() {
                "add-flathub" => rakuos_flatpak::add_flathub(system),
                "toggle" => rakuos_flatpak::set_remote_enabled(&name, enabled, system),
                "remove" => rakuos_flatpak::remove_remote(&name, system),
                _ => (false, format!("Unknown Flatpak action: {action_name}")),
            }
        },
        |(ok, status)| action::app(Message::FlatpakActionFinished(ok, status)),
    )
}

fn run_flatpak_add_remote(name: String, url: String, system: bool) -> Task<Message> {
    Task::perform(
        async move { rakuos_flatpak::add_remote(&name, &url, system) },
        |(ok, status)| action::app(Message::FlatpakActionFinished(ok, status)),
    )
}

fn add_custom_webapp(
    name: String,
    url: String,
    description: String,
    category: String,
    icon_source: String,
) -> Task<Message> {
    Task::perform(
        async move {
            rakuos_webapps::install_custom(
                &name,
                &url,
                &description,
                &category,
                &icon_source,
            )
        },
        |(ok, status)| action::app(Message::CustomWebAppFinished(ok, status)),
    )
}

fn pick_custom_webapp_icon() -> Task<Message> {
    Task::perform(
        async {
            use ashpd::desktop::file_chooser::{FileFilter, SelectedFiles};

            let response = SelectedFiles::open_file()
                .title("Choose Icon")
                .accept_label("Choose")
                .modal(true)
                .multiple(false)
                .filter(
                    FileFilter::new("Image files")
                        .mimetype("image/*")
                        .glob("*.png")
                        .glob("*.jpg")
                        .glob("*.jpeg")
                        .glob("*.svg")
                        .glob("*.ico")
                        .glob("*.webp"),
                )
                .send()
                .await
                .map_err(|error| format!("Could not open file picker portal: {error}"))?
                .response()
                .map_err(|error| format!("No icon selected: {error}"))?;

            response
                .uris()
                .first()
                .and_then(|uri| uri.to_file_path().ok())
                .map(|path| path.to_string_lossy().to_string())
                .filter(|path| !path.trim().is_empty())
                .ok_or_else(|| "No icon selected.".to_string())
        },
        |result| action::app(Message::CustomWebAppIconSelected(result)),
    )
}

fn load_firmware_data() -> Task<Message> {
    Task::perform(
        async {
            let available = rakuos_firmware::fwupd_available();
            FirmwareData {
                available,
                remotes: if available { rakuos_firmware::get_remotes() } else { Vec::new() },
                vendor_dirs: if available { rakuos_firmware::get_vendor_dirs() } else { Vec::new() },
                updates: if available { rakuos_firmware::get_updates() } else { Vec::new() },
            }
        },
        |data| action::app(Message::FirmwareLoaded(data)),
    )
}

fn run_firmware_toggle(id: String, enabled: bool) -> Task<Message> {
    Task::perform(
        async move { rakuos_firmware::set_remote_enabled(&id, enabled) },
        |(ok, status)| action::app(Message::FirmwareActionFinished(ok, status)),
    )
}

fn run_firmware_refresh() -> Task<Message> {
    Task::perform(
        async {
            let mut exit_code = 0;
            let mut lines = Vec::new();
            for line in rakuos_firmware::refresh_stream() {
                if let Some(code) = line.strip_prefix("__done__") {
                    exit_code = code.trim().parse().unwrap_or(1);
                } else if !line.trim().is_empty() {
                    lines.push(line);
                }
            }
            let ok = exit_code == 0;
            let status = if ok {
                "Firmware metadata refreshed.".to_string()
            } else {
                lines.last().cloned().unwrap_or_else(|| "Firmware refresh failed.".to_string())
            };
            (ok, status)
        },
        |(ok, status)| action::app(Message::FirmwareActionFinished(ok, status)),
    )
}

fn save_appimage_settings(
    id: String,
    update_source: String,
    update_url: String,
    update_pattern: String,
) -> Task<Message> {
    Task::perform(
        async move {
            rakuos_appimages::save_settings(
                &id,
                &update_source,
                &update_url,
                &update_pattern,
            )
        },
        |(ok, status)| action::app(Message::AppImageSettingsSaved(ok, status)),
    )
}

fn load_updates_cache() -> Task<Message> {
    Task::perform(
        tokio::task::spawn_blocking(|| {
            std::fs::read_to_string(daemon_cache_path())
                .ok()
                .and_then(|text| serde_json::from_str::<UpdatesData>(&text).ok())
                .map(enrich_updates_data)
        }),
        |res| action::app(Message::UpdatesCacheLoaded(res.unwrap_or(None))),
    )
}

fn enrich_updates_data(mut data: UpdatesData) -> UpdatesData {
    data.packages = rakuos_updates::enrich_package_updates(data.packages);
    data
}

fn check_updates() -> Task<Message> {
    Task::perform(
        async {
            // All update checks are blocking (subprocess / network). Run them on
            // tokio's blocking thread pool in parallel so the async executor
            // (libcosmic uses worker_threads(1)) is never starved.
            let (pkg_res, fp_res, web_res, ai_res, img_res) = tokio::join!(
                tokio::task::spawn_blocking(|| {
                    rakuos_updates::enrich_package_updates(rakuos_updates::check_packages_script())
                }),
                tokio::task::spawn_blocking(|| {
                    rakuos_flatpak::get_all_updates()
                        .into_iter()
                        .filter_map(|u| serde_json::to_value(u).ok())
                        .collect::<Vec<_>>()
                }),
                tokio::task::spawn_blocking(|| {
                    rakuos_webapps::get_updates()
                        .into_iter()
                        .filter_map(|u| serde_json::to_value(u).ok())
                        .collect::<Vec<_>>()
                }),
                tokio::task::spawn_blocking(|| {
                    rakuos_appimages::check_updates()
                        .into_iter()
                        .filter_map(|u| serde_json::to_value(u).ok())
                        .collect::<Vec<_>>()
                }),
                tokio::task::spawn_blocking(rakuos_updates::check_image_script),
            );

            let packages        = pkg_res.unwrap_or_default();
            let flatpak         = fp_res.unwrap_or_default();
            let webapps         = web_res.unwrap_or_default();
            let appimages       = ai_res.unwrap_or_default();
            let (image_available, image_info) = img_res.unwrap_or_default();
            let reboot_required = image_info["reboot_required"].as_bool().unwrap_or(false);
            let total = packages.len()
                + flatpak.len()
                + webapps.len()
                + appimages.len()
                + usize::from(image_available);

            UpdatesData {
                packages,
                flatpak,
                webapps,
                appimages,
                image_available,
                reboot_required,
                image_info,
                total,
            }
        },
        |data| action::app(Message::UpdatesChecked(Ok(data))),
    )
}

fn flatpak_update_stream_for(update: &serde_json::Value) -> Box<dyn Iterator<Item = String> + Send> {
    let app_id = json_string(update, "app_id").unwrap_or_default();
    let is_runtime = update["runtime"].as_bool().unwrap_or(false);
    let needs_install = update["needs_install"].as_bool().unwrap_or(false);
    let remote = json_string(update, "remote").unwrap_or_default();
    let branch = json_string(update, "version").unwrap_or_default();
    if app_id.is_empty() {
        return Box::new(std::iter::once("__done__1".to_string()));
    }
    if is_runtime && needs_install {
        Box::new(rakuos_flatpak::install_ref_stream(&remote, &app_id, &branch))
    } else if is_runtime {
        Box::new(rakuos_flatpak::update_runtime_stream(&app_id, &branch))
    } else {
        Box::new(rakuos_flatpak::update_single_stream(&app_id))
    }
}

fn run_update_target(target: UpdateTarget, data: Option<UpdatesData>, progress: Arc<AtomicU32>) -> Task<Message> {
    Task::perform(
        tokio::task::spawn_blocking(move || {
            let mut ok = true;
            let mut lines = Vec::new();

            match &target {
                UpdateTarget::All => {
                    if let Some(data) = data {
                        if !data.packages.is_empty() {
                            collect_update_stream(&mut lines, &mut ok, rakuos_updates::upgrade_packages_stream());
                        }
                        for update in &data.flatpak {
                            collect_update_stream(&mut lines, &mut ok, flatpak_update_stream_for(update));
                        }
                        for update in data.webapps {
                            if let Some(id) = json_string(&update, "id") {
                                let (success, status) = rakuos_webapps::update(&id);
                                ok &= success;
                                lines.push(status);
                            }
                        }
                        for update in data.appimages {
                            if let (Some(id), Some(url)) = (
                                json_string(&update, "id"),
                                json_string(&update, "download_url"),
                            ) {
                                collect_update_stream(
                                    &mut lines,
                                    &mut ok,
                                    rakuos_appimages::update_appimage_stream(&id, &url),
                                );
                            }
                        }
                        if data.image_available {
                            upgrade_image_from_info(&mut lines, &mut ok, &data.image_info, &progress);
                        }
                    }
                }
                UpdateTarget::Packages => {
                    collect_update_stream(&mut lines, &mut ok, rakuos_updates::upgrade_packages_stream());
                }
                UpdateTarget::Package(name) => {
                    collect_update_stream(&mut lines, &mut ok, rakuos_updates::upgrade_single_package_stream(name));
                }
                UpdateTarget::Flatpak => {
                    if let Some(data) = data {
                        for update in &data.flatpak {
                            collect_update_stream(&mut lines, &mut ok, flatpak_update_stream_for(update));
                        }
                    } else {
                        collect_update_stream(&mut lines, &mut ok, rakuos_flatpak::update_stream());
                    }
                }
                UpdateTarget::FlatpakApp(id) => {
                    collect_update_stream(&mut lines, &mut ok, rakuos_flatpak::update_single_stream(id));
                }
                UpdateTarget::FlatpakRuntime { app_id, branch } => {
                    collect_update_stream(&mut lines, &mut ok, rakuos_flatpak::update_runtime_stream(app_id, branch));
                }
                UpdateTarget::FlatpakRuntimeInstall { remote, app_id, branch } => {
                    collect_update_stream(&mut lines, &mut ok, rakuos_flatpak::install_ref_stream(remote, app_id, branch));
                }
                UpdateTarget::WebApps => {
                    for update in rakuos_webapps::get_updates() {
                        let (success, status) = rakuos_webapps::update(&update.id);
                        ok &= success;
                        lines.push(status);
                    }
                }
                UpdateTarget::WebApp(id) => {
                    let (success, status) = rakuos_webapps::update(id);
                    ok &= success;
                    lines.push(status);
                }
                UpdateTarget::AppImage { id, url } => {
                    collect_update_stream(&mut lines, &mut ok, rakuos_appimages::update_appimage_stream(id, url));
                }
                UpdateTarget::Image => {
                    if let Some(data) = data {
                        upgrade_image_from_info(&mut lines, &mut ok, &data.image_info, &progress);
                    }
                }
            }

            let status = lines
                .iter()
                .rev()
                .find(|line| !line.starts_with("__done__") && !line.trim().is_empty())
                .cloned()
                .unwrap_or_else(|| {
                    if ok {
                        "Update completed.".to_string()
                    } else {
                        "Update failed.".to_string()
                    }
                });
            (target, ok, status)
        }),
        |res| match res {
            Ok((target, ok, status)) => action::app(Message::UpdateFinished(target, ok, status)),
            Err(_) => action::app(Message::UpdateFinished(UpdateTarget::All, false, "Update failed.".to_string())),
        },
    )
}

fn collect_update_stream<I>(lines: &mut Vec<String>, ok: &mut bool, stream: I)
where
    I: IntoIterator<Item = String>,
{
    let mut saw_done = false;
    for line in stream {
        if let Some(code) = line.strip_prefix("__done__") {
            saw_done = true;
            *ok &= code.trim() == "0";
        } else if !line.trim().is_empty() {
            lines.push(line);
        }
    }
    if !saw_done {
        *ok &= true;
    }
}

fn upgrade_image_from_info(lines: &mut Vec<String>, ok: &mut bool, image_info: &serde_json::Value, progress: &Arc<AtomicU32>) {
    let update_type = image_info["type"].as_str().unwrap_or("upgrade").to_string();
    let repo_url = image_info["repo"].as_str().unwrap_or("").to_string();
    let new_tag = image_info["available"].as_str().unwrap_or("").to_string();
    for line in rakuos_updates::upgrade_image_stream(&update_type, &repo_url, &new_tag) {
        if let Some(code) = line.strip_prefix("__done__") {
            *ok &= code.trim() == "0";
            progress.store(100, Ordering::Relaxed);
        } else {
            if let Some(pct) = parse_bootc_progress_cosmic(&line) {
                progress.store(pct, Ordering::Relaxed);
            }
            if !line.trim().is_empty() {
                lines.push(line);
            }
        }
    }
}

fn parse_bootc_progress_cosmic(line: &str) -> Option<u32> {
    // [N/M] bracket format
    let trimmed = line.trim();
    if let Some(bs) = trimmed.find('[') {
        if let Some(be) = trimmed[bs..].find(']') {
            let inner = &trimmed[bs + 1..bs + be];
            if let Some(slash) = inner.find('/') {
                let n: f64 = inner[..slash].trim().parse().ok()?;
                let m: f64 = inner[slash + 1..].trim().parse().ok()?;
                if m > 0.0 { return Some(((n / m) * 100.0).min(100.0) as u32); }
            }
        }
    }
    // "N of M" pattern
    if let Some(of_pos) = line.find(" of ") {
        let n_str = line[..of_pos].trim().split_whitespace().last().unwrap_or("");
        let m_str = line[of_pos + 4..].trim().split_whitespace().next().unwrap_or("");
        if let (Ok(n), Ok(m)) = (n_str.parse::<f64>(), m_str.parse::<f64>()) {
            if m > 0.0 { return Some(((n / m) * 100.0).min(100.0) as u32); }
        }
    }
    // Raw "N/M" token near "layer"
    if line.to_ascii_lowercase().contains("layer") {
        for token in line.split_whitespace() {
            let tok = token.trim_matches(|c: char| !c.is_ascii_digit() && c != '/');
            if let Some(slash) = tok.find('/') {
                if let (Ok(n), Ok(m)) = (tok[..slash].parse::<f64>(), tok[slash + 1..].parse::<f64>()) {
                    if m > 0.0 { return Some(((n / m) * 100.0).min(100.0) as u32); }
                }
            }
        }
    }
    None
}

fn update_target_status(target: &UpdateTarget) -> String {
    match target {
        UpdateTarget::All => "Updating everything...".to_string(),
        UpdateTarget::Packages => "Updating packages...".to_string(),
        UpdateTarget::Package(name) => format!("Updating {name}..."),
        UpdateTarget::Flatpak => "Updating Flatpaks...".to_string(),
        UpdateTarget::FlatpakApp(id) => format!("Updating {id}..."),
        UpdateTarget::FlatpakRuntime { app_id, .. } => format!("Updating runtime {app_id}..."),
        UpdateTarget::FlatpakRuntimeInstall { app_id, branch, .. } => format!("Installing runtime {app_id}//{branch}..."),
        UpdateTarget::WebApps => "Updating web apps...".to_string(),
        UpdateTarget::WebApp(id) => format!("Updating {id}..."),
        UpdateTarget::AppImage { id, .. } => format!("Updating {id}..."),
        UpdateTarget::Image => "Updating system image...".to_string(),
    }
}

fn mark_update_complete(
    completed: &mut HashSet<String>,
    image_done: &mut bool,
    target: &UpdateTarget,
    data: Option<&UpdatesData>,
) {
    match target {
        UpdateTarget::All => {
            if let Some(data) = data {
                for update in &data.packages {
                    if let Some(name) = json_string(update, "name") {
                        completed.insert(package_key(&name));
                    }
                }
                for update in &data.flatpak {
                    if let Some(id) = json_string(update, "id").or_else(|| json_string(update, "app_id")) {
                        completed.insert(flatpak_key(&id));
                    }
                }
                for update in &data.webapps {
                    if let Some(id) = json_string(update, "id") {
                        completed.insert(webapp_key(&id));
                    }
                }
                for update in &data.appimages {
                    if let Some(id) = json_string(update, "id") {
                        completed.insert(appimage_key(&id));
                    }
                }
                if data.image_available {
                    *image_done = true;
                }
            }
        }
        UpdateTarget::Packages => {
            if let Some(data) = data {
                for update in &data.packages {
                    if let Some(name) = json_string(update, "name") {
                        completed.insert(package_key(&name));
                    }
                }
            }
        }
        UpdateTarget::Package(name) => {
            completed.insert(package_key(name));
        }
        UpdateTarget::Flatpak => {
            if let Some(data) = data {
                for update in &data.flatpak {
                    if let Some(id) = json_string(update, "id").or_else(|| json_string(update, "app_id")) {
                        completed.insert(flatpak_key(&id));
                    }
                }
            }
        }
        UpdateTarget::FlatpakApp(id) => {
            completed.insert(flatpak_key(id));
        }
        UpdateTarget::FlatpakRuntime { app_id, .. } => {
            completed.insert(flatpak_key(app_id));
        }
        UpdateTarget::FlatpakRuntimeInstall { app_id, .. } => {
            completed.insert(flatpak_key(app_id));
        }
        UpdateTarget::WebApps => {
            if let Some(data) = data {
                for update in &data.webapps {
                    if let Some(id) = json_string(update, "id") {
                        completed.insert(webapp_key(&id));
                    }
                }
            }
        }
        UpdateTarget::WebApp(id) => {
            completed.insert(webapp_key(id));
        }
        UpdateTarget::AppImage { id, .. } => {
            completed.insert(appimage_key(id));
        }
        UpdateTarget::Image => {
            *image_done = true;
        }
    }
}

pub(crate) fn remaining_update_count(
    data: Option<&UpdatesData>,
    completed: &HashSet<String>,
    image_done: bool,
) -> usize {
    let Some(data) = data else {
        return 0;
    };
    data.packages
        .iter()
        .filter_map(|update| json_string(update, "name"))
        .filter(|name| !completed.contains(&package_key(name)))
        .count()
        + data
            .flatpak
            .iter()
            .filter_map(|update| json_string(update, "id").or_else(|| json_string(update, "app_id")))
            .filter(|id| !completed.contains(&flatpak_key(id)))
            .count()
        + data
            .webapps
            .iter()
            .filter_map(|update| json_string(update, "id"))
            .filter(|id| !completed.contains(&webapp_key(id)))
            .count()
        + data
            .appimages
            .iter()
            .filter_map(|update| json_string(update, "id"))
            .filter(|id| !completed.contains(&appimage_key(id)))
            .count()
        + usize::from(data.image_available && !image_done)
}

fn write_updates_cache(data: &UpdatesData) {
    let path = daemon_cache_path();
    if let Some(parent) = path.parent() {
        let _ = std::fs::create_dir_all(parent);
    }
    if let Ok(json) = serde_json::to_string_pretty(data) {
        let _ = std::fs::write(path, json);
    }
}

fn write_badge_count(count: usize) {
    let _ = std::fs::write(badge_count_path(), count.to_string());
}

fn daemon_cache_path() -> PathBuf {
    std::env::var_os("HOME")
        .map(PathBuf::from)
        .unwrap_or_else(|| PathBuf::from("/tmp"))
        .join(".cache/rakuos/daemon-update-cache.json")
}

fn badge_count_path() -> PathBuf {
    std::env::temp_dir().join("rakuos-software-badge-count")
}

fn pid_file() -> PathBuf {
    std::env::var_os("HOME")
        .map(PathBuf::from)
        .unwrap_or_else(|| PathBuf::from("/tmp"))
        .join(".cache/rakuos/software-ui.pid")
}

fn write_pid_file() {
    let path = pid_file();
    if let Some(parent) = path.parent() {
        let _ = std::fs::create_dir_all(parent);
    }
    let _ = std::fs::write(path, std::process::id().to_string());
}

fn instance_already_running() -> bool {
    let Some(pid) = std::fs::read_to_string(pid_file())
        .ok()
        .and_then(|pid| pid.trim().parse::<u32>().ok())
    else {
        return false;
    };
    let cmdline = std::fs::read_to_string(format!("/proc/{pid}/cmdline")).unwrap_or_default();
    cmdline.contains("rakuos-software-cosmic")
}

fn check_trigger_path() -> PathBuf {
    std::env::temp_dir().join("rakuos-software-check-requested")
}

fn show_flag_path() -> PathBuf {
    std::env::temp_dir().join("rakuos-software-show")
}

fn quit_flag_path() -> PathBuf {
    std::env::temp_dir().join("rakuos-software-quit")
}

fn open_file_flag_path() -> PathBuf {
    std::env::temp_dir().join("rakuos-software-open-file")
}

fn consume_check_trigger() -> bool {
    let path = check_trigger_path();
    if path.exists() {
        let _ = std::fs::remove_file(path);
        true
    } else {
        false
    }
}

fn consume_show_request() -> bool {
    let path = show_flag_path();
    if path.exists() {
        let _ = std::fs::remove_file(path);
        true
    } else {
        false
    }
}

fn consume_quit_request() -> bool {
    let path = quit_flag_path();
    if path.exists() {
        let _ = std::fs::remove_file(path);
        true
    } else {
        false
    }
}

fn clear_stale_daemon_requests() {
    let _ = std::fs::remove_file(quit_flag_path());
    let _ = std::fs::remove_file(show_flag_path());
}

fn consume_open_file_request() -> Option<String> {
    let path = open_file_flag_path();
    let raw = std::fs::read_to_string(&path).ok()?;
    let _ = std::fs::remove_file(path);
    let normalized = normalize_file_arg(raw.trim());
    if normalized.is_empty() {
        None
    } else {
        Some(normalized)
    }
}

pub(crate) fn json_string(value: &serde_json::Value, key: &str) -> Option<String> {
    value[key].as_str().map(ToString::to_string).filter(|s| !s.is_empty())
}

pub(crate) fn json_bool(value: &serde_json::Value, key: &str) -> bool {
    value[key].as_bool().unwrap_or(false)
}

pub(crate) fn package_key(name: &str) -> String {
    format!("package:{name}")
}

pub(crate) fn flatpak_key(id: &str) -> String {
    format!("flatpak:{id}")
}

pub(crate) fn webapp_key(id: &str) -> String {
    format!("webapp:{id}")
}

pub(crate) fn appimage_key(id: &str) -> String {
    format!("appimage:{id}")
}

fn load_reviews(app_id: String) -> Task<Message> {
    Task::perform(
        async move { rakuos_reviews::get_reviews(&app_id).await },
        |reviews| action::app(Message::ReviewsLoaded(reviews)),
    )
}

fn submit_review(
    app_id: String,
    summary: String,
    body: String,
    rating: i32,
    version: String,
    user_display: Option<String>,
) -> Task<Message> {
    Task::perform(
        async move {
            rakuos_reviews::submit_review(
                &app_id,
                &summary,
                &body,
                rating,
                &version,
                user_display.as_deref(),
            )
            .await
        },
        |result| action::app(Message::ReviewSubmitted(result)),
    )
}

fn load_webapp_catalog() -> Vec<NativeApp> {
    let mut apps: Vec<_> = rakuos_webapps::get_catalog()
        .into_iter()
        .map(native_from_webapp)
        .collect();
    sort_apps(&mut apps);
    apps
}

fn load_webapp_search(query: &str) -> Vec<NativeApp> {
    let mut apps: Vec<_> = rakuos_webapps::search(query)
        .into_iter()
        .map(native_from_webapp)
        .collect();
    sort_apps(&mut apps);
    apps
}

fn load_categories(categories: &[&str]) -> anyhow::Result<Vec<NativeApp>> {
    let mut apps = Vec::new();
    for category in categories {
        apps.extend(rakuos_packages::get_by_category(category)?);
    }
    dedupe_apps(&mut apps);
    apps.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase()));
    Ok(apps)
}

fn load_installed() -> anyhow::Result<Vec<NativeApp>> {
    let mut apps = rakuos_packages::get_installed()?;
    apps.extend(rakuos_packages::get_installed_flatpaks_enriched()?);
    dedupe_apps(&mut apps);
    apps.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase()));
    Ok(apps)
}

fn load_installed_data() -> InstalledData {
    let mut data = InstalledData::default();

    data.rpm_apps = rakuos_packages::get_installed().unwrap_or_default();
    data.flatpak_apps = rakuos_packages::get_installed_flatpaks_enriched().unwrap_or_default();
    data.flatpak_runtimes = rakuos_packages::get_installed_flatpak_runtimes();
    data.app_images = rakuos_appimages::get_installed()
        .into_iter()
        .map(native_from_appimage)
        .collect();
    data.web_apps = rakuos_webapps::get_installed()
        .into_iter()
        .map(native_from_webapp)
        .collect();

    sort_apps(&mut data.rpm_apps);
    sort_apps(&mut data.flatpak_apps);
    sort_apps(&mut data.app_images);
    sort_apps(&mut data.web_apps);
    data
}

async fn run_operation(
    app_id: String,
    source: SourceOption,
    kind: OperationKind,
) -> (bool, String, Option<NativeApp>) {
    if source.source == "webapp" {
        let id = if source.id.is_empty() {
            source.package_name.clone()
        } else {
            source.id.clone()
        };
        let (ok, msg) = match kind {
            OperationKind::Install => rakuos_webapps::install(&id),
            OperationKind::Remove => rakuos_webapps::uninstall(&id),
        };
        return (ok, msg, find_webapp_native(&id));
    }

    let lines: Vec<String> = match (source.source.as_str(), &kind) {
        ("flatpak", OperationKind::Install) => rakuos_flatpak::install_stream(&source.id, &source.remote, !source.user_remote).collect(),
        ("flatpak", OperationKind::Remove) => rakuos_flatpak::uninstall_stream(&source.id, !source.user_remote).collect(),
        (_, OperationKind::Install) => rakuos_packages::install_stream(&source.package_name).collect(),
        (_, OperationKind::Remove) => rakuos_packages::remove_stream(&source.package_name).collect(),
    };

    let ok = lines
        .iter()
        .rev()
        .find_map(|line| line.strip_prefix("__done__"))
        .map(|code| code.trim() == "0")
        .unwrap_or(true);
    let msg = lines
        .iter()
        .rev()
        .find(|line| !line.starts_with("__done__") && !line.trim().is_empty())
        .cloned()
        .unwrap_or_else(|| {
            if ok {
                "Operation completed.".to_string()
            } else {
                "Operation failed.".to_string()
            }
        });
    let refreshed = rakuos_packages::get_app_by_id(&app_id).ok().flatten();
    (ok, msg, refreshed)
}

fn find_webapp_native(id: &str) -> Option<NativeApp> {
    rakuos_webapps::get_installed()
        .into_iter()
        .chain(rakuos_webapps::get_catalog())
        .find(|app| app.id == id)
        .map(native_from_webapp)
}

fn dedupe_apps(apps: &mut Vec<NativeApp>) {
    let mut seen = std::collections::HashSet::new();
    apps.retain(|app| {
        let key = app.name.to_lowercase();
        if seen.contains(&key) {
            false
        } else {
            seen.insert(key);
            true
        }
    });
}

fn sort_apps(apps: &mut [NativeApp]) {
    apps.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase()));
}

fn native_from_appimage(app: rakuos_appimages::AppImage) -> NativeApp {
    NativeApp {
        id: app.id.clone(),
        name: app.name,
        summary: app.summary,
        description: app.description,
        version: app.version,
        package_name: app.id,
        icon: String::new(),
        icon_url: String::new(),
        icon_path: app.icon_path,
        categories: app.categories,
        keywords: Vec::new(),
        screenshots: Vec::new(),
        developer: String::new(),
        url_homepage: app.update_url.clone(),
        url_bugtracker: String::new(),
        url_donation: String::new(),
        url_help: String::new(),
        url_faq: String::new(),
        url_vcs_browser: String::new(),
        url_contribute: String::new(),
        license: String::new(),
        source: "appimage".to_string(),
        installed: true,
        is_addon: false,
        pkg_name_guessed: false,
        update_source: app.update_source,
        update_url: app.update_url,
        update_pattern: app.update_pattern,
        sources: Vec::new(),
    }
}

fn native_from_webapp(app: rakuos_webapps::WebApp) -> NativeApp {
    let installed = app.installed || rakuos_webapps::is_installed(&app.id);
    NativeApp {
        id: app.id.clone(),
        name: app.name,
        summary: app.summary,
        description: app.description,
        version: app.last_updated,
        package_name: app.id,
        icon: String::new(),
        icon_url: app.icon_url,
        icon_path: app.icon_path,
        categories: app.categories,
        keywords: app.keywords,
        screenshots: app.screenshots,
        developer: String::new(),
        url_homepage: if app.website.is_empty() { app.url } else { app.website },
        url_bugtracker: String::new(),
        url_donation: String::new(),
        url_help: String::new(),
        url_faq: String::new(),
        url_vcs_browser: String::new(),
        url_contribute: String::new(),
        license: String::new(),
        source: "webapp".to_string(),
        installed,
        is_addon: false,
        pkg_name_guessed: false,
        update_source: String::new(),
        update_url: String::new(),
        update_pattern: String::new(),
        sources: Vec::new(),
    }
}

fn preferred_source_index(app: &NativeApp) -> usize {
    if app.sources.len() <= 1 {
        return 0;
    }

    if let Some(index) = app.sources.iter().position(|source| {
        matches!(source.source.as_str(), "native" | "terra") || source.label == "RakuOS Linux"
    }) {
        return index;
    }

    app.sources
        .iter()
        .position(|source| source.installed)
        .unwrap_or(0)
}

fn app_supports_reviews(app: &NativeApp, selected_index: usize) -> bool {
    let source = if app.sources.is_empty() {
        app.source.as_str()
    } else {
        app.sources
            .get(selected_index)
            .or_else(|| app.sources.first())
            .map(|source| source.source.as_str())
            .unwrap_or(app.source.as_str())
    };

    matches!(source, "native" | "terra" | "flatpak")
}

fn selected_source(app: &NativeApp, index: usize) -> SourceOption {
    if app.sources.is_empty() {
        SourceOption {
            id: app.id.clone(),
            source: app.source.clone(),
            package_name: app.package_name.clone(),
            installed: app.installed,
            label: source_label(&app.source),
            remote: String::new(),
            user_remote: false,
            summary: app.summary.clone(),
            description: app.description.clone(),
            version: app.version.clone(),
            developer: app.developer.clone(),
            url_homepage: app.url_homepage.clone(),
            url_bugtracker: app.url_bugtracker.clone(),
            url_donation: app.url_donation.clone(),
            url_help: app.url_help.clone(),
            url_faq: app.url_faq.clone(),
            url_vcs_browser: app.url_vcs_browser.clone(),
            url_contribute: app.url_contribute.clone(),
            license: app.license.clone(),
            screenshots: app.screenshots.clone(),
            icon_path: app.icon_path.clone(),
            icon_url: app.icon_url.clone(),
        }
    } else {
        app.sources
            .get(index)
            .cloned()
            .or_else(|| app.sources.first().cloned())
            .unwrap_or_default()
    }
}

fn source_label(source: &str) -> String {
    match source {
        "native" | "terra" => "RakuOS Linux".to_string(),
        "flatpak" => "Flathub".to_string(),
        "webapp" => "Web App".to_string(),
        "local-rpm" => "Local RPM".to_string(),
        other => other.to_string(),
    }
}

fn daemon_binary() -> Option<PathBuf> {
    let current = std::env::current_exe().ok()?;
    let dir = current.parent()?;
    let sibling = dir.join("rakuos-software-tray");
    if sibling.exists() {
        Some(sibling)
    } else {
        Some(PathBuf::from("/usr/bin/rakuos-software-tray"))
    }
}

fn ensure_daemon_running() {
    let running = std::process::Command::new("pgrep")
        .args(["-f", "rakuos-software-tray"])
        .status()
        .map(|status| status.success())
        .unwrap_or(false);
    if running {
        return;
    }

    if let Some(binary) = daemon_binary() {
        let _ = std::process::Command::new(binary)
            .env("RAKUOS_SOFTWARE_FRONTEND", "cosmic")
            .spawn();
    }
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    env_logger::init();
    let startup_file = std::env::args()
        .skip(1)
        .find(|arg| !arg.starts_with('-'))
        .map(|arg| normalize_file_arg(&arg))
        .filter(|path| local_file_kind(path).is_some());

    if instance_already_running() {
        if let Some(path) = startup_file {
            let _ = std::fs::write(open_file_flag_path(), path);
        }
        let _ = std::fs::write(show_flag_path(), "1");
        return Ok(());
    }

    write_pid_file();
    clear_stale_daemon_requests();
    ensure_daemon_running();
    let start_hidden = std::env::args().any(|a| a == "--tray");
    let mut settings = Settings::default()
        .size(Size::new(1000.0, 700.0))
        .size_limits(LayoutLimits::NONE.min_width(800.0).min_height(600.0))
        .is_daemon(false);
    if start_hidden {
        settings = settings.no_main_window(true);
    }
    cosmic::app::run::<SoftwareApp>(settings, ())?;
    let _ = std::fs::remove_file(pid_file());
    Ok(())
}
