// ui-qt/backend.rs — QObject backend exposed to QML

#![allow(non_snake_case)]

use qmetaobject::prelude::*;
use std::collections::hash_map::DefaultHasher;
use std::ffi::{CStr, CString};
use std::hash::{Hash, Hasher};
use std::os::raw::c_char;
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, AtomicI32, Ordering};
use std::sync::Arc;

// ── Cache readiness flag (set by warmCache thread) ───────────────────────────
static CACHE_READY: AtomicBool = AtomicBool::new(false);

// ── Reviews async state (separate from main op so they don't conflict) ────────
static REVIEWS_RUNNING: AtomicBool = AtomicBool::new(false);
static REVIEW_SUBMIT_RUNNING: AtomicBool = AtomicBool::new(false);

unsafe extern "C" {
    fn plasma_addon_categories_json() -> *mut c_char;
    fn plasma_addons_browse_json(category: *const c_char, query: *const c_char, page: i32) -> *mut c_char;
    fn plasma_addons_installed_json() -> *mut c_char;
    fn plasma_addons_updates_json() -> *mut c_char;
    fn plasma_addon_install_json(id: *const c_char) -> *mut c_char;
    fn plasma_addon_remove_json(id: *const c_char) -> *mut c_char;
    fn free_qt_json(value: *mut c_char);
}

fn take_qt_json(ptr: *mut c_char) -> String {
    if ptr.is_null() {
        return String::new();
    }
    let value = unsafe { CStr::from_ptr(ptr).to_string_lossy().into_owned() };
    unsafe { free_qt_json(ptr) };
    value
}

fn cstring_lossy(value: &str) -> CString {
    CString::new(value.replace('\0', "")).unwrap_or_default()
}

fn is_plasma_session_env() -> bool {
    let haystack = [
        std::env::var("XDG_CURRENT_DESKTOP").unwrap_or_default(),
        std::env::var("DESKTOP_SESSION").unwrap_or_default(),
        std::env::var("KDE_FULL_SESSION").unwrap_or_default(),
    ]
    .join(":")
    .to_ascii_lowercase();
    haystack.contains("kde") || haystack.contains("plasma")
}

fn reviews_log_path() -> std::path::PathBuf {
    std::env::temp_dir().join("rakuos-reviews.json")
}

fn review_submit_log_path() -> std::path::PathBuf {
    std::env::temp_dir().join("rakuos-review-submit.json")
}

// ── Install queue entry ───────────────────────────────────────────────────────

#[derive(Clone)]
pub struct InstallQueueEntry {
    pub app_name: String,
    pub id: String,
    pub source: String,
    pub remote: String,
    pub is_remove: bool,
    pub icon_path: String,
    pub icon_url: String,
}

// ── Shared async state ────────────────────────────────────────────────────────

#[derive(Default)]
pub struct SharedState {
    pub running:  AtomicBool,
    /// 0=idle 1=success 2=failed
    pub result:   AtomicI32,
    pub progress: AtomicI32,  // 0-100
}

fn log_path() -> std::path::PathBuf {
    std::env::temp_dir().join("rakuos-software-qt.log")
}

fn plasma_installed_log_path() -> std::path::PathBuf {
    std::env::temp_dir().join("rakuos-software-qt-plasma-installed.log")
}

fn settings_path() -> std::path::PathBuf {
    let home = std::env::var("HOME").unwrap_or_else(|_| "/root".to_string());
    std::path::PathBuf::from(home).join(".config/rakuos/software-settings.json")
}



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

fn daemon_cache_path() -> std::path::PathBuf {
    let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
    std::path::PathBuf::from(home).join(".cache/rakuos/daemon-update-cache.json")
}

fn user_cache_root() -> PathBuf {
    if let Ok(path) = std::env::var("XDG_CACHE_HOME") {
        if !path.is_empty() {
            return PathBuf::from(path).join("rakuos/software");
        }
    }
    let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
    PathBuf::from(home).join(".cache/rakuos/software")
}

fn plasma_addon_cache_dir() -> PathBuf {
    user_cache_root().join("plasma-addons")
}

fn safe_cache_key(value: &str) -> String {
    let mut hasher = DefaultHasher::new();
    value.hash(&mut hasher);
    format!("{:016x}", hasher.finish())
}

fn plasma_addon_categories_cache_path() -> PathBuf {
    plasma_addon_cache_dir().join("categories.json")
}

fn plasma_addon_page_cache_path(category: &str, page: i32) -> PathBuf {
    plasma_addon_cache_dir()
        .join("pages")
        .join(format!("{}-{}.json", safe_cache_key(category), page.max(0)))
}

fn plasma_addon_icon_cache_dir() -> PathBuf {
    plasma_addon_cache_dir().join("icons")
}

fn icon_cache_extension(url: &str) -> &'static str {
    let clean = url.split('?').next().unwrap_or(url).to_ascii_lowercase();
    if clean.ends_with(".svg") {
        "svg"
    } else if clean.ends_with(".webp") {
        "webp"
    } else if clean.ends_with(".jpg") || clean.ends_with(".jpeg") {
        "jpg"
    } else if clean.ends_with(".gif") {
        "gif"
    } else {
        "png"
    }
}

fn plasma_addon_icon_cache_path(url: &str) -> PathBuf {
    plasma_addon_icon_cache_dir().join(format!(
        "{}.{}",
        safe_cache_key(url),
        icon_cache_extension(url)
    ))
}

fn write_cache(path: PathBuf, json: &str) {
    if let Some(parent) = path.parent() {
        let _ = std::fs::create_dir_all(parent);
    }
    let _ = std::fs::write(path, json);
}

fn download_to_cache(url: &str, path: &PathBuf) -> bool {
    if path.exists() {
        return true;
    }
    if let Some(parent) = path.parent() {
        let _ = std::fs::create_dir_all(parent);
    }

    let Ok(runtime) = tokio::runtime::Builder::new_current_thread()
        .enable_all()
        .build()
    else {
        return false;
    };

    let result = runtime.block_on(async {
        let client = reqwest::Client::builder()
            .timeout(std::time::Duration::from_secs(12))
            .build()
            .ok()?;
        let response = client.get(url).send().await.ok()?;
        if !response.status().is_success() {
            return None;
        }
        let bytes = response.bytes().await.ok()?;
        if bytes.is_empty() || bytes.len() > 5 * 1024 * 1024 {
            return None;
        }
        Some(bytes)
    });

    if let Some(bytes) = result {
        std::fs::write(path, bytes).is_ok()
    } else {
        false
    }
}

fn cache_plasma_addon_icons(json: &str, download_missing: bool) -> String {
    let Ok(mut addons) = serde_json::from_str::<Vec<serde_json::Value>>(json) else {
        return json.to_string();
    };

    for addon in &mut addons {
        let Some(url) = addon.get("icon_url").and_then(|v| v.as_str()) else { continue };
        if !url.starts_with("http://") && !url.starts_with("https://") {
            continue;
        }

        let path = plasma_addon_icon_cache_path(url);
        if path.exists() || (download_missing && download_to_cache(url, &path)) {
            addon["icon_path"] = path.to_string_lossy().to_string().into();
        }
    }

    serde_json::to_string(&addons).unwrap_or_else(|_| json.to_string())
}

fn plasma_addon_ids_from_json(json: &str) -> Vec<String> {
    serde_json::from_str::<Vec<serde_json::Value>>(json)
        .unwrap_or_default()
        .into_iter()
        .filter_map(|addon| {
            addon
                .get("id")
                .or_else(|| addon.get("kns_id"))
                .and_then(|value| value.as_str())
                .map(|value| value.to_string())
        })
        .collect()
}

fn fetch_plasma_addon_page_json(category: &str, page: i32) -> String {
    let cat = cstring_lossy(category);
    let query = cstring_lossy("");
    unsafe {
        take_qt_json(plasma_addons_browse_json(cat.as_ptr(), query.as_ptr(), page.max(0)))
    }
}

fn warm_plasma_addon_category(category: &str) {
    let mut seen = std::collections::HashSet::new();
    for page in 0..50 {
        let raw = fetch_plasma_addon_page_json(category, page);
        if raw.is_empty() {
            break;
        }

        let ids = plasma_addon_ids_from_json(&raw);
        if ids.is_empty() {
            break;
        }

        let mut new_ids = 0;
        for id in ids {
            if seen.insert(id) {
                new_ids += 1;
            }
        }
        if new_ids == 0 {
            break;
        }

        let json = cache_plasma_addon_icons(&raw, true);
        write_cache(plasma_addon_page_cache_path(category, page), &json);

        // Keep the background crawl polite; users can still browse while this fills.
        std::thread::sleep(std::time::Duration::from_millis(75));
    }
}

fn collect_plasma_addon_leaf_ids(nodes: &[serde_json::Value], out: &mut Vec<String>) {
    for node in nodes {
        let Some(id) = node.get("id").and_then(|v| v.as_str()) else { continue };
        let subs = node.get("subs").and_then(|v| v.as_array());
        if let Some(subs) = subs {
            if subs.is_empty() {
                out.push(id.to_string());
            } else {
                collect_plasma_addon_leaf_ids(subs, out);
            }
        } else {
            out.push(id.to_string());
        }
    }
}

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

fn append_log(text: &str) {
    use std::io::Write;
    if let Ok(mut f) = std::fs::OpenOptions::new().append(true).open(log_path()) {
        let _ = f.write_all(text.as_bytes());
        let _ = f.write_all(b"\n");
    }
}

// ── Backend QObject ───────────────────────────────────────────────────────────

#[derive(QObject, Default)]
pub struct SoftwareBackend {
    base: qt_base_class!(trait QObject),

    // Navigation
    currentPage: qt_property!(i32; NOTIFY currentPageChanged),
    currentPageChanged: qt_signal!(),

    // Operation state (for showing progress/log area)
    opRunning: qt_property!(bool; NOTIFY opStateChanged),
    opResult:  qt_property!(i32;  NOTIFY opStateChanged),   // 0=idle 1=ok 2=fail
    opProgress: qt_property!(i32; NOTIFY opStateChanged),   // 0-100
    logRevision: qt_property!(i32; NOTIFY logRevisionChanged),
    logRevisionChanged: qt_signal!(),
    opStateChanged: qt_signal!(),

    // Search
    searchQuery: qt_property!(QString; NOTIFY searchQueryChanged),
    searchQueryChanged: qt_signal!(),

    // Cached JSON strings — UI reads these as JS objects via JSON.parse
    homeDataJson:      qt_property!(QString; NOTIFY homeDataChanged),
    homeDataChanged:   qt_signal!(),
    installedJson:     qt_property!(QString; NOTIFY installedChanged),
    installedChanged:  qt_signal!(),
    updatesJson:       qt_property!(QString; NOTIFY updatesChanged),
    updatesChanged:    qt_signal!(),
    searchResultsJson: qt_property!(QString; NOTIFY searchResultsChanged),
    searchResultsChanged: qt_signal!(),
    systemStatusJson:  qt_property!(QString; NOTIFY systemStatusChanged),
    systemStatusChanged: qt_signal!(),
    settingsJson:      qt_property!(QString; NOTIFY settingsChanged),
    settingsChanged:   qt_signal!(),

    // Background update badge count (populated by daemon cache or startup check)
    pendingUpdateCount: qt_property!(i32; NOTIFY pendingUpdateCountChanged),
    pendingUpdateCountChanged: qt_signal!(),

    // Install queue state (banner at window bottom)
    queueCount:           qt_property!(i32;     NOTIFY queueStateChanged),
    queueActiveName:      qt_property!(QString; NOTIFY queueStateChanged),
    queueActiveIconPath:  qt_property!(QString; NOTIFY queueStateChanged),
    queueActiveIconUrl:   qt_property!(QString; NOTIFY queueStateChanged),
    queueIsRemove:        qt_property!(bool;    NOTIFY queueStateChanged),
    queueStateChanged: qt_signal!(),

    // Shared state between Rust threads and Qt
    shared: Option<Arc<SharedState>>,

    // Install/remove queue — main thread only, no Arc needed.
    // NOT a qt_property; managed entirely in Rust.
    install_queue: std::collections::VecDeque<InstallQueueEntry>,
    // Tracks previous opRunning so pollOp can detect the idle transition.
    prev_op_running: bool,
    // True when the currently-running op is an install/remove (vs home load etc).
    op_is_install_remove: bool,

    // ── Navigation ────────────────────────────────────────────────────────────

    navigate: qt_method!(fn navigate(&mut self, page: i32) {
        self.currentPage = page;
        self.currentPageChanged();
    }),

    pollOp: qt_method!(fn pollOp(&mut self) {
        let s = self.get_shared();
        let running  = s.running.load(Ordering::Relaxed);
        let result   = s.result.load(Ordering::Relaxed);
        let progress = s.progress.load(Ordering::Relaxed);

        let changed = self.opRunning != running
            || self.opResult != result
            || self.opProgress != progress;

        if changed {
            self.opRunning  = running;
            self.opResult   = result;
            self.opProgress = progress;
            self.opStateChanged();
        }

        // Detect install/remove completion → dequeue next.
        if self.prev_op_running && !running && self.op_is_install_remove {
            self.op_is_install_remove = false;
            self.dequeue_next_installop();
        }
        self.prev_op_running = running;

        // Poll log file revision
        if let Ok(m) = std::fs::metadata(log_path()) {
            let rev = m.len() as i32;
            if self.logRevision != rev {
                self.logRevision = rev;
                self.logRevisionChanged();
            }
        }
    }),

    pollQueue: qt_method!(fn pollQueue(&mut self) {
        // QML calls this on a slower timer to refresh the banner properties.
        // Nothing to do — properties are already updated synchronously when
        // installApp/removeApp enqueue or dequeue_next_installop fires.
        let _ = self;
    }),

    readLog: qt_method!(fn readLog(&mut self) -> QString {
        std::fs::read_to_string(log_path())
            .unwrap_or_default()
            .into()
    }),

    // ── Home page ─────────────────────────────────────────────────────────────

    loadHome: qt_method!(fn loadHome(&mut self) {
        self.start_op();
        let shared = self.get_shared();
        std::thread::spawn(move || {
            let rt = tokio::runtime::Runtime::new().unwrap();
            let result = rt.block_on(async {
                let (picks, popular, updated, new) = rakuos_home::load_all().await;
                serde_json::json!({
                    "picks":   picks,
                    "popular": popular,
                    "updated": updated,
                    "new":     new,
                })
            });
            let _ = std::fs::write(
                log_path(),
                serde_json::to_string_pretty(&result).unwrap_or_default(),
            );
            shared.result.store(1, Ordering::Relaxed);
            shared.running.store(false, Ordering::Relaxed);
        });
    }),

    homeDataLoaded: qt_method!(fn homeDataLoaded(&mut self) -> QString {
        std::fs::read_to_string(log_path()).unwrap_or_default().into()
    }),

    // ── Installed page ────────────────────────────────────────────────────────

    loadInstalled: qt_method!(fn loadInstalled(&mut self) {
        self.start_op();
        let shared = self.get_shared();
        std::thread::spawn(move || {
            let mut all: Vec<serde_json::Value> = Vec::new();

            // Flatpak installed — enriched with AppStream
            if let Ok(apps) = rakuos_packages::get_installed_flatpaks_enriched() {
                for a in apps {
                    all.push(serde_json::to_value(a).unwrap_or_default());
                }
            }
            // Native/overlay installed
            if let Ok(apps) = rakuos_packages::get_installed() {
                for a in apps {
                    all.push(serde_json::to_value(a).unwrap_or_default());
                }
            }
            for a in rakuos_webapps::get_installed() {
                all.push(serde_json::to_value(a).unwrap_or_default());
            }
            for a in rakuos_appimages::get_installed() {
                all.push(serde_json::to_value(a).unwrap_or_default());
            }

            let _ = std::fs::write(
                log_path(),
                serde_json::to_string(&all).unwrap_or_default(),
            );
            shared.result.store(1, Ordering::Relaxed);
            shared.running.store(false, Ordering::Relaxed);
        });
    }),

    loadPlasmaAddonsInstalled: qt_method!(fn loadPlasmaAddonsInstalled(&mut self) {
        self.start_op();
        let shared = self.get_shared();
        std::thread::spawn(move || {
            let mut all: Vec<serde_json::Value> = Vec::new();
            if is_plasma_session_env() {
                let json = unsafe { take_qt_json(plasma_addons_installed_json()) };
                if let Ok(addons) = serde_json::from_str::<Vec<serde_json::Value>>(&json) {
                    all.extend(addons);
                }
            }
            let _ = std::fs::write(
                plasma_installed_log_path(),
                serde_json::to_string(&all).unwrap_or_default(),
            );
            shared.result.store(1, Ordering::Relaxed);
            shared.running.store(false, Ordering::Relaxed);
        });
    }),

    readPlasmaInstalledLog: qt_method!(fn readPlasmaInstalledLog(&mut self) -> QString {
        std::fs::read_to_string(plasma_installed_log_path()).unwrap_or_default().into()
    }),

    // ── App detail lookup ─────────────────────────────────────────────────────

    loadAppById: qt_method!(fn loadAppById(&mut self, app_id: QString) {
        let app_id = app_id.to_string();
        self.start_op();
        let shared = self.get_shared();
        std::thread::spawn(move || {
            let result = match rakuos_packages::get_app_by_id(&app_id) {
                Ok(Some(app)) => serde_json::to_string(&app).unwrap_or_default(),
                _ => "null".to_string(),
            };
            let _ = std::fs::write(log_path(), &result);
            shared.result.store(1, Ordering::Relaxed);
            shared.running.store(false, Ordering::Relaxed);
        });
    }),

    // ── Screenshot download / cache ───────────────────────────────────────────


    // ── Add-ons lookup (sync — appstream cache is already in memory) ──────────

    getAddons: qt_method!(fn getAddons(&mut self, app_id: QString, source_type: QString) -> QString {
        let app_id = app_id.to_string();
        let source_type = source_type.to_string();
        match rakuos_packages::get_addons_for_app(&app_id, &source_type) {
            Ok(addons) => QString::from(serde_json::to_string(&addons).unwrap_or_else(|_| "[]".into())),
            Err(_) => QString::from("[]"),
        }
    }),

    // ── Installed Flatpak runtimes/add-ons (sync) ────────────────────────────

    loadFlatpakRuntimes: qt_method!(fn loadFlatpakRuntimes(&mut self) -> QString {
        let runtimes = rakuos_packages::get_installed_flatpak_runtimes();
        QString::from(serde_json::to_string(&runtimes).unwrap_or_else(|_| "[]".to_string()))
    }),

    plasmaAddonCategories: qt_method!(fn plasmaAddonCategories(&mut self) -> QString {
        if !is_plasma_session_env() {
            return "[]".into();
        }
        let cache_path = plasma_addon_categories_cache_path();
        let json = unsafe { take_qt_json(plasma_addon_categories_json()) };
        if !json.is_empty() && json != "[]" {
            write_cache(cache_path, &json);
            return json.into();
        }
        std::fs::read_to_string(cache_path).unwrap_or_else(|_| "[]".to_string()).into()
    }),

    isPlasmaSession: qt_method!(fn isPlasmaSession(&mut self) -> bool {
        is_plasma_session_env()
    }),

    warmPlasmaAddonCache: qt_method!(fn warmPlasmaAddonCache(&mut self) {
        if !is_plasma_session_env() {
            return;
        }
        std::thread::spawn(|| {
            let categories_json = unsafe { take_qt_json(plasma_addon_categories_json()) };
            if categories_json.is_empty() || categories_json == "[]" {
                return;
            }
            write_cache(plasma_addon_categories_cache_path(), &categories_json);

            let Ok(tree) = serde_json::from_str::<Vec<serde_json::Value>>(&categories_json) else {
                return;
            };
            let mut ids = Vec::new();
            collect_plasma_addon_leaf_ids(&tree, &mut ids);
            ids.sort();
            ids.dedup();

            for id in ids {
                warm_plasma_addon_category(&id);
            }
            log::info!("Plasma add-on cache warmed.");
        });
    }),

    // ── Web App catalog ───────────────────────────────────────────────────────

    loadWebAppCatalog: qt_method!(fn loadWebAppCatalog(&mut self) {
        self.start_op();
        let shared = self.get_shared();
        std::thread::spawn(move || {
            let apps = rakuos_webapps::get_catalog();
            let json = serde_json::to_string(&apps).unwrap_or_default();
            let _ = std::fs::write(log_path(), &json);
            shared.result.store(1, Ordering::Relaxed);
            shared.running.store(false, Ordering::Relaxed);
        });
    }),

    /// Install a user-defined custom web app.
    /// icon_source: local file path or HTTP(S) URL, or empty for no icon.
    installCustomWebApp: qt_method!(fn installCustomWebApp(
        &mut self,
        name:        QString,
        url:         QString,
        description: QString,
        category:    QString,
        icon_source: QString,
    ) {
        self.start_op();
        let shared = self.get_shared();
        let name        = name.to_string();
        let url         = url.to_string();
        let description = description.to_string();
        let category    = category.to_string();
        let icon_source = icon_source.to_string();
        std::thread::spawn(move || {
            let (ok, msg) = rakuos_webapps::install_custom(
                &name, &url, &description, &category, &icon_source,
            );
            append_log(&msg);
            shared.result.store(if ok { 1 } else { 0 }, Ordering::Relaxed);
            shared.running.store(false, Ordering::Relaxed);
        });
    }),

    launchWebApp: qt_method!(fn launchWebApp(&mut self, app_id: QString) {
        let id = app_id.to_string();
        std::thread::spawn(move || {
            let _ = std::process::Command::new("gtk-launch")
                .arg(format!("rakuos-webapp-{}", id))
                .spawn();
        });
    }),

    // ── AppImage settings ─────────────────────────────────────────────────────

    // ── Local file install ────────────────────────────────────────────────────

    /// Store the startup file path (set from argv in main.rs before QML loads).
    startupFilePath: qt_property!(QString; NOTIFY startupFilePathChanged),
    startupFilePathChanged: qt_signal!(),

    /// Read the startup file path from the temp flag written by main.rs.
    /// QML calls this once at Component.onCompleted; clears the flag after reading.
    readStartupFilePath: qt_method!(fn readStartupFilePath(&mut self) -> QString {
        let flag = std::env::temp_dir().join("rakuos-software-open-file");
        if let Ok(path) = std::fs::read_to_string(&flag) {
            let _ = std::fs::remove_file(&flag);
            let path = path.trim().to_string();
            if !path.is_empty() {
                self.startupFilePath = path.clone().into();
                self.startupFilePathChanged();
                return path.into();
            }
        }
        "".into()
    }),

    /// Determine file kind from extension.
    fileKind: qt_method!(fn fileKind(&mut self, path: QString) -> QString {
        let p = path.to_string().to_lowercase();
        if p.ends_with(".rpm")        { return "rpm".into(); }
        if p.ends_with(".flatpak")    { return "flatpak".into(); }
        if p.ends_with(".flatpakref") { return "flatpakref".into(); }
        if p.ends_with(".appimage")   { return "appimage".into(); }
        "unknown".into()
    }),

    /// Get metadata for a local file. Returns JSON.
    getLocalFileInfo: qt_method!(fn getLocalFileInfo(&mut self, path: QString, kind: QString) -> QString {
        let path = path.to_string();
        let kind = kind.to_string();
        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),
            _            => serde_json::json!({"error": "Unknown file type"}),
        };
        info.to_string().into()
    }),

    /// Install a local file. Uses start_op pattern; poll opRunning/opResult.
    installLocalFile: qt_method!(fn installLocalFile(&mut self, path: QString, kind: QString, action: QString, pkg_name: QString) {
        let path     = path.to_string();
        let kind     = kind.to_string();
        let action   = action.to_string();
        let pkg_name = pkg_name.to_string();
        self.start_op();
        let shared = self.get_shared();
        std::thread::spawn(move || {
            let _ = std::fs::write(log_path(), format!("Installing {}...\n", path));
            let ok = match kind.as_str() {
                "rpm" => {
                    let mut exit_code = 1i32;
                    let iter: Box<dyn Iterator<Item = String> + Send> = if action == "reinstall" {
                        Box::new(rakuos_packages::reinstall_local_rpm_stream(&pkg_name, &path))
                    } else {
                        Box::new(rakuos_packages::install_local_rpm_stream(&path))
                    };
                    for line in iter {
                        if let Some(code) = line.strip_prefix("__done__") {
                            exit_code = code.trim().parse().unwrap_or(1);
                        } else if !line.is_empty() {
                            append_log(&line);
                        }
                    }
                    exit_code == 0
                }
                "flatpak" => {
                    let mut exit_code = 1i32;
                    for line in rakuos_flatpak::install_local_bundle_stream(&path) {
                        if let Some(code) = line.strip_prefix("__done__") {
                            exit_code = code.trim().parse().unwrap_or(1);
                        } else if !line.is_empty() {
                            append_log(&line);
                        }
                    }
                    exit_code == 0
                }
                "flatpakref" => {
                    let mut exit_code = 1i32;
                    for line in rakuos_flatpak::install_flatpakref_stream(&path) {
                        if let Some(code) = line.strip_prefix("__done__") {
                            exit_code = code.trim().parse().unwrap_or(1);
                        } else if !line.is_empty() {
                            append_log(&line);
                        }
                    }
                    exit_code == 0
                }
                "appimage" => {
                    let (ok, msg, _) = rakuos_appimages::install_appimage(&path);
                    append_log(&msg);
                    ok
                }
                _ => { append_log("Unknown file type"); false }
            };
            shared.result.store(if ok { 1 } else { 2 }, Ordering::Relaxed);
            shared.running.store(false, Ordering::Relaxed);
        });
    }),

    // ── AppImage settings ─────────────────────────────────────────────────────

    saveAppImageSettings: qt_method!(fn saveAppImageSettings(&mut self, id: QString, update_source: QString, update_url: QString, update_pattern: QString) -> QString {
        let (ok, msg) = rakuos_appimages::save_settings(
            &id.to_string(),
            &update_source.to_string(),
            &update_url.to_string(),
            &update_pattern.to_string(),
        );
        serde_json::json!({ "ok": ok, "msg": msg }).to_string().into()
    }),

    // ── Search ────────────────────────────────────────────────────────────────

    search: qt_method!(fn search(&mut self, query: QString) {
        let query = query.to_string();
        self.searchQuery = query.clone().into();
        self.searchQueryChanged();
        self.start_op();
        let shared = self.get_shared();
        std::thread::spawn(move || {
            let mut results: Vec<serde_json::Value> = Vec::new();

            if let Ok(apps) = rakuos_packages::search(&query) {
                for a in apps { results.push(serde_json::to_value(a).unwrap_or_default()); }
            }
            for a in rakuos_webapps::search(&query) {
                results.push(serde_json::to_value(a).unwrap_or_default());
            }
            // Search AppImages
            let q_lower = query.to_lowercase();
            for a in rakuos_appimages::get_installed() {
                let name = a.name.to_lowercase();
                let summary = a.description.to_lowercase();
                if name.contains(&q_lower) || summary.contains(&q_lower) {
                    results.push(serde_json::to_value(a).unwrap_or_default());
                }
            }

            let _ = std::fs::write(
                log_path(),
                serde_json::to_string(&results).unwrap_or_default(),
            );
            shared.result.store(1, Ordering::Relaxed);
            shared.running.store(false, Ordering::Relaxed);
        });
    }),

    loadCategory: qt_method!(fn loadCategory(&mut self, category: QString) {
        let category = category.to_string();
        self.start_op();
        let shared = self.get_shared();
        std::thread::spawn(move || {
            let mut results: Vec<serde_json::Value> = Vec::new();
            if let Some(kns_category) = category.strip_prefix("kns:") {
                let cache_path = plasma_addon_page_cache_path(kns_category, 0);
                let cached = std::fs::read_to_string(&cache_path).ok();
                let json = if let Some(cached) = cached {
                    cache_plasma_addon_icons(&cached, false)
                } else {
                    cache_plasma_addon_icons(&fetch_plasma_addon_page_json(kns_category, 0), false)
                };
                if let Ok(addons) = serde_json::from_str::<Vec<serde_json::Value>>(&json) {
                    results.extend(addons);
                }
            } else if let Ok(apps) = rakuos_packages::get_by_category(&category) {
                for a in apps { results.push(serde_json::to_value(a).unwrap_or_default()); }
            }
            let _ = std::fs::write(
                log_path(),
                serde_json::to_string(&results).unwrap_or_default(),
            );
            shared.result.store(1, Ordering::Relaxed);
            shared.running.store(false, Ordering::Relaxed);
        });
    }),

    loadPlasmaAddonCategoryPage: qt_method!(fn loadPlasmaAddonCategoryPage(&mut self, category: QString, page: i32) {
        let category = category.to_string();
        self.start_op();
        let shared = self.get_shared();
        std::thread::spawn(move || {
            let mut results: Vec<serde_json::Value> = Vec::new();
            let kns_category = category.strip_prefix("kns:").unwrap_or(&category);
            let page = page.max(0);
            let cache_path = plasma_addon_page_cache_path(kns_category, page);
            let cached = std::fs::read_to_string(&cache_path).ok();
            let json = if let Some(cached) = cached {
                cache_plasma_addon_icons(&cached, false)
            } else {
                cache_plasma_addon_icons(&fetch_plasma_addon_page_json(kns_category, page), false)
            };
            if let Ok(addons) = serde_json::from_str::<Vec<serde_json::Value>>(&json) {
                results.extend(addons);
            }
            let _ = std::fs::write(
                log_path(),
                serde_json::to_string(&results).unwrap_or_default(),
            );
            shared.result.store(1, Ordering::Relaxed);
            shared.running.store(false, Ordering::Relaxed);
        });
    }),

    // ── Updates ───────────────────────────────────────────────────────────────

    checkUpdates: qt_method!(fn checkUpdates(&mut self) {
        self.start_op();
        let shared = self.get_shared();
        std::thread::spawn(move || {
            use std::process::Command;

            let _ = std::fs::write(log_path(), "Checking for updates...\n");

            // Check packages via rakuos-update script
            let pkg_out = Command::new("/usr/libexec/rakuos/rakuos-update")
                .arg("check")
                .output()
                .ok();

            let pkg_updates: Vec<serde_json::Value> = rakuos_updates::enrich_package_updates(
                pkg_out
                    .and_then(|o| serde_json::from_slice::<serde_json::Value>(&o.stdout).ok())
                    .and_then(|v| v["updates"].as_array().cloned())
                    .unwrap_or_default(),
            );

            // Use get_all_updates() so flatpak entries are icon-enriched via the
            // AppStream cache (same path as the installed page).  Direct script
            // parsing skips that enrichment and leaves icon_path empty.
            let fp_updates: Vec<serde_json::Value> = rakuos_flatpak::get_all_updates()
                .into_iter()
                .filter_map(|f| serde_json::to_value(f).ok())
                .collect();

            let webapp_updates: Vec<serde_json::Value> = rakuos_webapps::get_updates()
                .into_iter()
                .filter_map(|w| serde_json::to_value(w).ok())
                .collect();

            let plasma_addon_updates: Vec<serde_json::Value> = if is_plasma_session_env() {
                let json = unsafe { take_qt_json(plasma_addons_updates_json()) };
                serde_json::from_str(&json).unwrap_or_default()
            } else {
                Vec::new()
            };

            // Check image update — also detects staged images waiting for reboot.
            let (image_available, image_info) = rakuos_updates::check_image_script();
            let reboot_required = image_info["reboot_required"].as_bool().unwrap_or(false);

            let total = pkg_updates.len() + fp_updates.len()
                + webapp_updates.len()
                + plasma_addon_updates.len()
                + if image_available { 1 } else { 0 };

            let result = serde_json::json!({
                "packages":        pkg_updates,
                "flatpak":         fp_updates,
                "webapps":         webapp_updates,
                "plasma_addons":   plasma_addon_updates,
                "appimages":       [],
                "image_available": image_available,
                "reboot_required": reboot_required,
                "image_info":      image_info,
                "total": total,
            });

            let result_json = serde_json::to_string_pretty(&result).unwrap_or_default();
            let _ = std::fs::write(log_path(), &result_json);

            // Write to daemon cache so the tray and update page share the same data
            let cache_path = daemon_cache_path();
            if let Some(parent) = cache_path.parent() {
                let _ = std::fs::create_dir_all(parent);
            }
            let _ = std::fs::write(&cache_path, &result_json);

            shared.result.store(1, Ordering::Relaxed);
            shared.running.store(false, Ordering::Relaxed);
        });
    }),

    // ── Image update ─────────────────────────────────────────────────────────

    upgradeImage: qt_method!(fn upgradeImage(&mut self, update_type: QString, repo_url: QString, new_tag: QString) {
        let update_type = update_type.to_string();
        let repo_url = repo_url.to_string();
        let new_tag = new_tag.to_string();
        self.start_op();
        let shared = self.get_shared();
        std::thread::spawn(move || {
            let _ = std::fs::write(log_path(), "Starting image upgrade...\n");
            let mut exit_code = 1i32;
            for line in rakuos_updates::upgrade_image_stream(&update_type, &repo_url, &new_tag) {
                if let Some(code) = line.strip_prefix("__done__") {
                    exit_code = code.trim().parse().unwrap_or(1);
                } else {
                    if let Some(pct) = parse_layers_progress(&line) {
                        shared.progress.store(pct, Ordering::Relaxed);
                    }
                    if !line.is_empty() { append_log(&line); }
                }
            }
            shared.result.store(if exit_code == 0 { 1 } else { 2 }, Ordering::Relaxed);
            shared.running.store(false, Ordering::Relaxed);
        });
    }),

    switchImageWithReset: qt_method!(fn switchImageWithReset(&mut self, repo_url: QString, new_tag: QString) {
        let repo_url = repo_url.to_string();
        let new_tag = new_tag.to_string();
        self.start_op();
        let shared = self.get_shared();
        std::thread::spawn(move || {
            let target = if !repo_url.is_empty() && !new_tag.is_empty() {
                format!("{}:{}", repo_url, new_tag)
            } else {
                String::new()
            };

            let _ = std::fs::write(log_path(), "Preparing image switch...\n");

            let mut exit_code = 1i32;
            if target.is_empty() {
                append_log("Image switch failed: missing target.");
            } else {
                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 let Some(pct) = parse_layers_progress(&line) {
                            shared.progress.store(pct, Ordering::Relaxed);
                        }
                        if !line.is_empty() { append_log(&line); }
                    }
                }
            }

            shared.result.store(if exit_code == 0 { 1 } else { 2 }, Ordering::Relaxed);
            shared.running.store(false, Ordering::Relaxed);
        });
    }),

    rollbackSystem: qt_method!(fn rollbackSystem(&mut self) {
        self.start_op();
        let shared = self.get_shared();
        std::thread::spawn(move || {
            let _ = std::fs::write(log_path(), "Rolling back...\n");
            let mut exit_code = 1i32;
            for line in rakuos_updates::rollback_stream() {
                if let Some(code) = line.strip_prefix("__done__") {
                    exit_code = code.trim().parse().unwrap_or(1);
                } else {
                    if !line.is_empty() { append_log(&line); }
                }
            }
            shared.result.store(if exit_code == 0 { 1 } else { 2 }, Ordering::Relaxed);
            shared.running.store(false, Ordering::Relaxed);
        });
    }),

    rebootSystem: qt_method!(fn rebootSystem(&mut self) {
        std::thread::spawn(|| {
            rakuos_updates::schedule_reboot();
        });
    }),

    getOverlayStatus: qt_method!(fn getOverlayStatus(&mut self) -> QString {
        let status = rakuos_updates::get_overlay_status();
        serde_json::to_string(&status).unwrap_or_default().into()
    }),

    upgradePackages: qt_method!(fn upgradePackages(&mut self) {
        self.start_op();
        let shared = self.get_shared();
        std::thread::spawn(move || {
            let _ = std::fs::write(log_path(), "Upgrading overlay packages...\n");
            let mut exit_code = 1i32;
            for line in rakuos_updates::upgrade_packages_stream() {
                if let Some(code) = line.strip_prefix("__done__") {
                    exit_code = code.trim().parse().unwrap_or(1);
                } else {
                    // Parse [N/M] progress from dnf5 transaction output
                    if let Some(pct) = parse_layers_progress(&line) {
                        shared.progress.store(pct, Ordering::Relaxed);
                    }
                    if !line.is_empty() { append_log(&line); }
                }
            }
            shared.result.store(if exit_code == 0 { 1 } else { 2 }, Ordering::Relaxed);
            shared.running.store(false, Ordering::Relaxed);
        });
    }),

    upgradePackage: qt_method!(fn upgradePackage(&mut self, name: QString) {
        let name = name.to_string();
        self.start_op();
        let shared = self.get_shared();
        std::thread::spawn(move || {
            let _ = std::fs::write(log_path(), format!("Upgrading package {}...\n", name));
            let mut exit_code = 1i32;
            for line in rakuos_updates::upgrade_single_package_stream(&name) {
                if let Some(code) = line.strip_prefix("__done__") {
                    exit_code = code.trim().parse().unwrap_or(1);
                } else {
                    if let Some(pct) = parse_layers_progress(&line) {
                        shared.progress.store(pct, Ordering::Relaxed);
                    }
                    if !line.is_empty() { append_log(&line); }
                }
            }
            shared.result.store(if exit_code == 0 { 1 } else { 2 }, Ordering::Relaxed);
            shared.running.store(false, Ordering::Relaxed);
        });
    }),

    updateWebApp: qt_method!(fn updateWebApp(&mut self, id: QString) {
        let id = id.to_string();
        self.start_op();
        let shared = self.get_shared();
        std::thread::spawn(move || {
            let _ = std::fs::write(log_path(), format!("Updating web app {}...\n", id));
            let ok = if id == "__all__" {
                let mut all_ok = true;
                for app in rakuos_webapps::get_updates() {
                    let (app_ok, msg) = rakuos_webapps::update(&app.id);
                    if !msg.is_empty() {
                        append_log(&msg);
                    }
                    if !app_ok {
                        all_ok = false;
                    }
                }
                all_ok
            } else {
                let (ok, msg) = rakuos_webapps::update(&id);
                if !msg.is_empty() {
                    append_log(&msg);
                }
                ok
            };
            if ok {
                append_log("Web app update complete.");
            }
            shared.result.store(if ok { 1 } else { 2 }, Ordering::Relaxed);
            shared.running.store(false, Ordering::Relaxed);
        });
    }),

    updatePlasmaAddons: qt_method!(fn updatePlasmaAddons(&mut self) {
        self.start_op();
        let shared = self.get_shared();
        std::thread::spawn(move || {
            let _ = std::fs::write(log_path(), "Updating Plasma add-ons...\n");
            let updates_json = unsafe { take_qt_json(plasma_addons_updates_json()) };
            let updates: Vec<serde_json::Value> = serde_json::from_str(&updates_json).unwrap_or_default();
            let mut all_ok = true;
            for addon in updates {
                if let Some(id) = addon["id"].as_str() {
                    append_log(&format!("Updating {}...", addon["name"].as_str().unwrap_or(id)));
                    let id_c = cstring_lossy(id);
                    let raw = unsafe { take_qt_json(plasma_addon_install_json(id_c.as_ptr())) };
                    let val: serde_json::Value = serde_json::from_str(&raw).unwrap_or_default();
                    if let Some(msg) = val["msg"].as_str() {
                        append_log(msg);
                    }
                    if !val["ok"].as_bool().unwrap_or(false) {
                        all_ok = false;
                    }
                }
            }
            shared.result.store(if all_ok { 1 } else { 2 }, Ordering::Relaxed);
            shared.running.store(false, Ordering::Relaxed);
        });
    }),

    // ── Install / Remove ─────────────────────────────────────────────────────

    installApp: qt_method!(fn installApp(&mut self, id: QString, source: QString, remote: QString) {
        let id = id.to_string();
        let source = source.to_string();
        let remote = remote.to_string();

        // If an install/remove is already running, enqueue for later.
        if self.op_is_install_remove || self.opRunning {
            let (display, ip, iu) = lookup_app_info(&id);
            self.install_queue.push_back(InstallQueueEntry {
                app_name: display,
                id,
                source,
                remote,
                is_remove: false,
                icon_path: ip,
                icon_url: iu,
            });
            self.queueCount = self.install_queue.len() as i32;
            self.queueStateChanged();
            return;
        }

        self.op_is_install_remove = true;
        let (display_name, icon_path, icon_url) = lookup_app_info(&id);
        self.queueActiveName = display_name.into();
        self.queueActiveIconPath = icon_path.into();
        self.queueActiveIconUrl = icon_url.into();
        self.queueIsRemove = false;
        self.queueStateChanged();
        self.start_op();
        let shared = self.get_shared();
        std::thread::spawn(move || {
            use std::io::{BufRead, BufReader};
            use std::process::{Command, Stdio};
            let _ = std::fs::write(log_path(), format!("Installing {}...\n", id));

            let (mut child, ok) = match source.as_str() {
                "plasma-addon" => {
                    let id_c = cstring_lossy(&id);
                    let raw = unsafe { take_qt_json(plasma_addon_install_json(id_c.as_ptr())) };
                    let val: serde_json::Value = serde_json::from_str(&raw).unwrap_or_default();
                    if let Some(msg) = val["msg"].as_str() {
                        append_log(msg);
                    }
                    (None, val["ok"].as_bool().unwrap_or(false))
                }
                "flatpak" => {
                    let mut cmd = Command::new("flatpak");
                    if id == "__upgrade_all__" {
                        cmd.args(["update", "--noninteractive", "-y"]);
                    } else {
                        // Fresh install from detail page (or //branch runtime install).
                        let r = if remote.is_empty() { "flathub" } else { remote.as_str() };
                        let scope = if rakuos_flatpak::is_user_remote(r) { "--user" } else { "--system" };
                        cmd.args(["install", scope, "--noninteractive", "-y", r, &id]);
                    }
                    let c = cmd.stdout(Stdio::piped()).stderr(Stdio::piped()).spawn();
                    match c {
                        Ok(child) => (Some(child), true),
                        Err(e)    => { append_log(&e.to_string()); (None, false) }
                    }
                }
                "flatpak-update" => {
                    // Individual update from the updates page.
                    // Runtime patch updates arrive as "app_id//branch" — no --app flag.
                    // App updates arrive as plain "app_id" — use --app to avoid touching runtimes.
                    let mut cmd = Command::new("flatpak");
                    if id.contains("//") {
                        cmd.args(["update", "--noninteractive", "-y", &id]);
                    } else {
                        cmd.args(["update", "--app", "--noninteractive", "-y", &id]);
                    }
                    let c = cmd.stdout(Stdio::piped()).stderr(Stdio::piped()).spawn();
                    match c {
                        Ok(child) => (Some(child), true),
                        Err(e)    => { append_log(&e.to_string()); (None, false) }
                    }
                }
                "webapp" => {
                    let (ok, msg) = rakuos_webapps::install(&id);
                    append_log(&msg);
                    (None, ok)
                }
                _ => {
                    // Resolve AppStream ID → RPM package name
                    let appstream = rakuos_appstream::get_appstream();
                    let pkg = appstream.get(&id)
                        .or_else(|| appstream.get(&format!("native:{}", id)))
                        .map(|a| a.package_name.clone())
                        .filter(|p| !p.is_empty())
                        .unwrap_or_else(|| id.clone());
                    drop(appstream);
                    let c = Command::new("sudo")
                        .args(["/usr/libexec/rakuos/rakuos-install", &pkg])
                        .stdout(Stdio::piped())
                        .stderr(Stdio::piped())
                        .spawn();
                    match c {
                        Ok(child) => (Some(child), true),
                        Err(e)    => { append_log(&e.to_string()); (None, false) }
                    }
                }
            };

            if let Some(mut child) = child {
                // Drain stdout and parse progress; drain stderr concurrently.
                let stdout = child.stdout.take();
                let stderr = child.stderr.take();
                let stderr_thread = stderr.map(|s| {
                    std::thread::spawn(move || { for _ in BufReader::new(s).lines() {} })
                });
                // Fake-crawl thread: advances progress from 2→95 while process runs.
                let shared_crawl = Arc::clone(&shared);
                let crawl = std::thread::spawn(move || {
                    loop {
                        std::thread::sleep(std::time::Duration::from_millis(200));
                        if !shared_crawl.running.load(Ordering::Relaxed) { break; }
                        let cur = shared_crawl.progress.load(Ordering::Relaxed);
                        if cur < 95 {
                            // Real progress parsing may have jumped ahead; only advance.
                            let next = (cur + 1).min(95);
                            shared_crawl.progress.store(next, Ordering::Relaxed);
                        }
                    }
                });
                if let Some(stdout) = stdout {
                    for line in BufReader::new(stdout).lines().map_while(Result::ok) {
                        if !line.is_empty() {
                            append_log(&line);
                            if let Some(pct) = parse_install_progress(&line) {
                                let cur = shared.progress.load(Ordering::Relaxed);
                                if pct > cur { shared.progress.store(pct, Ordering::Relaxed); }
                            }
                        }
                    }
                }
                if let Some(t) = stderr_thread { t.join().ok(); }
                let success = child.wait().map(|s| s.success()).unwrap_or(false);
                shared.result.store(if success { 1 } else { 2 }, Ordering::Relaxed);
                // Signal done before joining crawl so it sees running=false and exits.
                shared.running.store(false, Ordering::Relaxed);
                crawl.join().ok();
                return;
            } else {
                shared.result.store(if ok { 1 } else { 2 }, Ordering::Relaxed);
            }
            shared.running.store(false, Ordering::Relaxed);
        });
    }),

    resetOverlay: qt_method!(fn resetOverlay(&mut self, mode: QString) {
        let mode = mode.to_string();
        self.start_op();
        let shared = self.get_shared();
        std::thread::spawn(move || {
            use std::process::Command;
            let flag = if mode == "soft" { "--soft" } else { "--confirm" };
            let _ = std::fs::write(log_path(), format!("Running overlay reset ({mode})...\n"));
            let out = Command::new("pkexec")
                .args(["/usr/libexec/rakuos/rakuos-reset-overlay", flag])
                .output();
            match out {
                Ok(o) => {
                    let msg = String::from_utf8_lossy(&o.stdout).to_string()
                        + &String::from_utf8_lossy(&o.stderr);
                    let _ = std::fs::write(log_path(), msg);
                    shared.result.store(if o.status.success() { 1 } else { 2 }, Ordering::Relaxed);
                }
                Err(e) => {
                    let _ = std::fs::write(log_path(), e.to_string());
                    shared.result.store(2, Ordering::Relaxed);
                }
            }
            shared.running.store(false, Ordering::Relaxed);
        });
    }),

    removeApp: qt_method!(fn removeApp(&mut self, id: QString, source: QString) {
        let id = id.to_string();
        let source = source.to_string();

        // If an install/remove is already running, enqueue for later.
        if self.op_is_install_remove || self.opRunning {
            let (display, ip, iu) = lookup_app_info(&id);
            self.install_queue.push_back(InstallQueueEntry {
                app_name: display,
                id,
                source,
                remote: String::new(),
                is_remove: true,
                icon_path: ip,
                icon_url: iu,
            });
            self.queueCount = self.install_queue.len() as i32;
            self.queueStateChanged();
            return;
        }

        self.op_is_install_remove = true;
        let (display_name, icon_path, icon_url) = lookup_app_info(&id);
        self.queueActiveName = display_name.into();
        self.queueActiveIconPath = icon_path.into();
        self.queueActiveIconUrl = icon_url.into();
        self.queueIsRemove = true;
        self.queueStateChanged();
        self.start_op();
        let shared = self.get_shared();
        std::thread::spawn(move || {
            use std::process::{Command, Stdio};
            use std::io::{BufRead, BufReader};
            let _ = std::fs::write(log_path(), format!("Removing {}...\n", id));

            // Fake-crawl: advance progress 2→95 while remove runs
            let shared_crawl = Arc::clone(&shared);
            let crawl = std::thread::spawn(move || {
                loop {
                    std::thread::sleep(std::time::Duration::from_millis(200));
                    if !shared_crawl.running.load(Ordering::Relaxed) { break; }
                    let cur = shared_crawl.progress.load(Ordering::Relaxed);
                    if cur < 95 {
                        shared_crawl.progress.store((cur + 1).min(95), Ordering::Relaxed);
                    }
                }
            });

            let ok = match source.as_str() {
                "plasma-addon" => {
                    let id_c = cstring_lossy(&id);
                    let raw = unsafe { take_qt_json(plasma_addon_remove_json(id_c.as_ptr())) };
                    let val: serde_json::Value = serde_json::from_str(&raw).unwrap_or_default();
                    if let Some(msg) = val["msg"].as_str() {
                        append_log(msg);
                    }
                    val["ok"].as_bool().unwrap_or(false)
                }
                "flatpak" | "flatpak-runtime" => {
                    let mut cmd = Command::new("flatpak");
                    let mut args = vec!["uninstall", "--noninteractive", "-y"];
                    if source == "flatpak-runtime" { args.push("--force-remove"); }
                    args.push(&id);
                    match cmd.args(&args).stdout(Stdio::piped()).stderr(Stdio::piped()).spawn() {
                        Ok(mut child) => {
                            let stdout = child.stdout.take();
                            let stderr = child.stderr.take();
                            let stderr_t = stderr.map(|s| {
                                std::thread::spawn(move || { for _ in BufReader::new(s).lines() {} })
                            });
                            if let Some(out) = stdout {
                                for line in BufReader::new(out).lines().map_while(Result::ok) {
                                    if !line.is_empty() { append_log(&line); }
                                }
                            }
                            if let Some(t) = stderr_t { t.join().ok(); }
                            child.wait().map(|s| s.success()).unwrap_or(false)
                        }
                        Err(e) => { append_log(&e.to_string()); false }
                    }
                }
                "webapp" => {
                    let (ok, msg) = rakuos_webapps::uninstall(&id);
                    append_log(&msg);
                    ok
                }
                "appimage" => {
                    let (ok, msg) = rakuos_appimages::uninstall(&id);
                    append_log(&msg);
                    ok
                }
                _ => {
                    let appstream = rakuos_appstream::get_appstream();
                    let pkg = appstream.get(&id)
                        .or_else(|| appstream.get(&format!("native:{}", id)))
                        .map(|a| a.package_name.clone())
                        .filter(|p| !p.is_empty())
                        .unwrap_or_else(|| id.clone());
                    drop(appstream);
                    match Command::new("sudo")
                        .args(["/usr/libexec/rakuos/rakuos-remove", &pkg])
                        .stdout(Stdio::piped()).stderr(Stdio::piped()).spawn()
                    {
                        Ok(mut child) => {
                            let stdout = child.stdout.take();
                            let stderr = child.stderr.take();
                            let stderr_t = stderr.map(|s| {
                                std::thread::spawn(move || { for _ in BufReader::new(s).lines() {} })
                            });
                            if let Some(out) = stdout {
                                for line in BufReader::new(out).lines().map_while(Result::ok) {
                                    if !line.is_empty() { append_log(&line); }
                                }
                            }
                            if let Some(t) = stderr_t { t.join().ok(); }
                            child.wait().map(|s| s.success()).unwrap_or(false)
                        }
                        Err(e) => { append_log(&e.to_string()); false }
                    }
                }
            };

            shared.result.store(if ok { 1 } else { 2 }, Ordering::Relaxed);
            shared.running.store(false, Ordering::Relaxed);
            crawl.join().ok();
        });
    }),

    // ── System status ─────────────────────────────────────────────────────────

    loadSystemStatus: qt_method!(fn loadSystemStatus(&mut self) {
        self.start_op();
        let shared = self.get_shared();
        std::thread::spawn(move || {
            let status = rakuos_updates::get_system_status();
            let overlay = rakuos_updates::get_overlay_status();
            let result = serde_json::json!({
                "image":     status.image,
                "version":   status.version,
                "digest":    status.digest,
                "timestamp": status.timestamp,
                "error":     status.error,
                "overlay_packages": overlay.packages,
                "overlay_count": overlay.package_count,
                "overlay_dirty": overlay.is_dirty,
            });
            let json = serde_json::to_string(&result).unwrap_or_default();
            let _ = std::fs::write(log_path(), &json);
            shared.result.store(1, Ordering::Relaxed);
            shared.running.store(false, Ordering::Relaxed);
        });
    }),

    upgradeSystem: qt_method!(fn upgradeSystem(&mut self) {
        self.start_op();
        let shared = self.get_shared();
        std::thread::spawn(move || {
            use std::io::{BufRead, BufReader};
            use std::process::{Command, Stdio};
            let _ = std::fs::write(log_path(), "Starting system upgrade...\n");

            let mut child = match Command::new("pkexec")
                .args(["bootc", "upgrade"])
                .stdout(Stdio::piped())
                .stderr(Stdio::piped())
                .spawn()
            {
                Ok(c) => c,
                Err(e) => {
                    append_log(&e.to_string());
                    shared.result.store(2, Ordering::Relaxed);
                    shared.running.store(false, Ordering::Relaxed);
                    return;
                }
            };

            if let Some(stdout) = child.stdout.take() {
                for line in BufReader::new(stdout).lines().map_while(Result::ok) {
                    if let Some(pct) = parse_layers_progress(&line) {
                        shared.progress.store(pct, Ordering::Relaxed);
                    }
                    if !line.is_empty() { append_log(&line); }
                }
            }

            let ok = child.wait().map(|s| s.success()).unwrap_or(false);
            shared.result.store(if ok { 1 } else { 2 }, Ordering::Relaxed);
            shared.running.store(false, Ordering::Relaxed);
        });
    }),

    // ── Flatpak remote management ─────────────────────────────────────────────

    getFlatpakRemotes: qt_method!(fn getFlatpakRemotes(&mut self) -> QString {
        let remotes = rakuos_flatpak::get_remotes();
        let has_flathub = rakuos_flatpak::has_flathub();
        serde_json::json!({ "remotes": remotes, "has_flathub": has_flathub })
            .to_string().into()
    }),

    addFlatpakRemote: qt_method!(fn addFlatpakRemote(&mut self, name: QString, url: QString, system: bool) -> QString {
        let (ok, msg) = rakuos_flatpak::add_remote(&name.to_string(), &url.to_string(), system);
        serde_json::json!({ "ok": ok, "msg": msg }).to_string().into()
    }),

    addFlathub: qt_method!(fn addFlathub(&mut self, system: bool) -> QString {
        let (ok, msg) = rakuos_flatpak::add_flathub(system);
        serde_json::json!({ "ok": ok, "msg": msg }).to_string().into()
    }),

    removeFlatpakRemote: qt_method!(fn removeFlatpakRemote(&mut self, name: QString, system: bool) -> QString {
        let (ok, msg) = rakuos_flatpak::remove_remote(&name.to_string(), system);
        serde_json::json!({ "ok": ok, "msg": msg }).to_string().into()
    }),

    setFlatpakRemoteEnabled: qt_method!(fn setFlatpakRemoteEnabled(&mut self, name: QString, enabled: bool, system: bool) -> QString {
        let (ok, msg) = rakuos_flatpak::set_remote_enabled(&name.to_string(), enabled, system);
        serde_json::json!({ "ok": ok, "msg": msg }).to_string().into()
    }),

    // ── Kernel arguments ──────────────────────────────────────────────────────

    getKernelArgs: qt_method!(fn getKernelArgs(&self) -> QString {
        let cmdline  = rakuos_updates::get_current_cmdline();
        let user_args = rakuos_updates::get_user_kernel_args();
        serde_json::json!({ "cmdline": cmdline, "user_args": user_args })
            .to_string().into()
    }),

    setKernelArgs: qt_method!(fn setKernelArgs(&mut self, options: QString) {
        let options = options.to_string();
        self.start_op();
        let shared = self.get_shared();
        std::thread::spawn(move || {
            use std::process::Command;
            let _ = std::fs::write(log_path(), "Applying kernel parameters…\n");
            let mut cmd = Command::new("pkexec");
            cmd.args(["bootc", "loader-entries", "set-options-for-source",
                      "--source", "rakuos-user"]);
            if !options.trim().is_empty() {
                cmd.args(["--options", options.trim()]);
            }
            match cmd.output() {
                Ok(o) => {
                    let msg = String::from_utf8_lossy(&o.stdout).to_string()
                        + &String::from_utf8_lossy(&o.stderr);
                    let _ = std::fs::write(log_path(), if msg.trim().is_empty() {
                        if o.status.success() { "Kernel args staged.".to_string() }
                        else { "Failed to apply kernel args.".to_string() }
                    } else { msg });
                    shared.result.store(if o.status.success() { 1 } else { 2 }, Ordering::Relaxed);
                }
                Err(e) => {
                    let _ = std::fs::write(log_path(), e.to_string());
                    shared.result.store(2, Ordering::Relaxed);
                }
            }
            shared.running.store(false, Ordering::Relaxed);
        });
    }),

    // ── Firmware management ───────────────────────────────────────────────────

    getFirmwareData: qt_method!(fn getFirmwareData(&mut self) -> QString {
        let available = rakuos_firmware::fwupd_available();
        let remotes     = if available { rakuos_firmware::get_remotes()     } else { vec![] };
        let vendor_dirs = if available { rakuos_firmware::get_vendor_dirs() } else { vec![] };
        let updates     = if available { rakuos_firmware::get_updates()     } else { vec![] };
        serde_json::json!({
            "available":   available,
            "remotes":     remotes,
            "vendor_dirs": vendor_dirs,
            "updates":     updates,
        }).to_string().into()
    }),

    setFirmwareRemoteEnabled: qt_method!(fn setFirmwareRemoteEnabled(&mut self, id: QString, enabled: bool) -> QString {
        let (ok, msg) = rakuos_firmware::set_remote_enabled(&id.to_string(), enabled);
        serde_json::json!({ "ok": ok, "msg": msg }).to_string().into()
    }),

    refreshFirmware: qt_method!(fn refreshFirmware(&mut self) {
        self.start_op();
        let shared = self.get_shared();
        std::thread::spawn(move || {
            let _ = std::fs::write(log_path(), "Refreshing firmware metadata...\n");
            let mut exit_code = 1i32;
            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.is_empty() { append_log(&line); }
                }
            }
            shared.result.store(if exit_code == 0 { 1 } else { 2 }, Ordering::Relaxed);
            shared.running.store(false, Ordering::Relaxed);
        });
    }),

    checkFirmwareUpdates: qt_method!(fn checkFirmwareUpdates(&mut self) -> QString {
        let updates = rakuos_firmware::get_updates();
        serde_json::to_string(&updates).unwrap_or_default().into()
    }),

    // ── Settings ─────────────────────────────────────────────────────────────

    loadSettings: qt_method!(fn loadSettings(&mut self) -> QString {
        std::fs::read_to_string(settings_path())
            .unwrap_or_else(|_| serde_json::json!({
                "update_interval": 1440,
                "auto_check_packages": true,
                "auto_check_flatpak": true,
                "auto_check_image": true,
                "auto_check_appimages": true,
                "auto_update": false,
            }).to_string())
            .into()
    }),

    saveSettings: qt_method!(fn saveSettings(&mut self, json: QString) {
        let path = settings_path();
        if let Some(parent) = path.parent() {
            let _ = std::fs::create_dir_all(parent);
        }
        let _ = std::fs::write(&path, json.to_string());
    }),

    getBootcAutoUpdate: qt_method!(fn getBootcAutoUpdate(&mut self) -> bool {
        rakuos_updates::is_bootc_autoupdate_enabled()
    }),

    setBootcAutoUpdate: qt_method!(fn setBootcAutoUpdate(&mut self, enable: bool) {
        rakuos_updates::set_bootc_autoupdate(enable);
    }),

    // ── Startup helpers ───────────────────────────────────────────────────────

    // Pre-warm the appstream cache in a background thread so first searches
    // and category loads are instant.
    warmCache: qt_method!(fn warmCache(&mut self) {
        std::thread::spawn(|| {
            let _ = rakuos_appstream::get_appstream();
            CACHE_READY.store(true, Ordering::Release);
            log::info!("AppStream cache warmed.");
        });
    }),

    isCacheReady: qt_method!(fn isCacheReady(&mut self) -> bool {
        CACHE_READY.load(Ordering::Acquire)
    }),

    // Returns true while rakuos-overlay-update holds its lock.
    // QML polls this on a timer and disables install buttons for repo packages.
    isOverlayUpdateRunning: qt_method!(fn isOverlayUpdateRunning(&mut self) -> bool {
        std::path::Path::new("/run/rakuos-overlay.lock").exists()
    }),

    // Returns true when running in a live ISO environment (liveuser session).
    // Used by QML to hide the System page from the sidebar.
    isLiveEnvironment: qt_method!(fn isLiveEnvironment(&mut self) -> bool {
        std::env::var("USER").unwrap_or_default() == "liveuser"
            || std::path::Path::new("/run/live").exists()
    }),

    // Check if main.rs wrote a "start hidden" flag (launched with --tray).
    // Returns true once (deletes the flag) so QML knows to stay hidden at startup.
    readStartHidden: qt_method!(fn readStartHidden(&mut self) -> bool {
        let flag = std::env::temp_dir().join("rakuos-software-start-hidden");
        if flag.exists() {
            let _ = std::fs::remove_file(&flag);
            return true;
        }
        false
    }),

    // Check if the tray daemon wrote a "show window" flag.
    // Returns true once (deletes the flag) so QML can show + activate the window.
    checkShowRequest: qt_method!(fn checkShowRequest(&mut self) -> bool {
        let flag = show_flag_path();
        if flag.exists() {
            let _ = std::fs::remove_file(&flag);
            return true;
        }
        false
    }),

    // Check if the tray daemon wrote a "quit" flag.
    // Returns true once (deletes the flag) so QML can call Qt.quit().
    checkQuitRequest: qt_method!(fn checkQuitRequest(&mut self) -> bool {
        let flag = std::env::temp_dir().join("rakuos-software-quit");
        if flag.exists() {
            let _ = std::fs::remove_file(&flag);
            return true;
        }
        false
    }),

    // Read cached update count written by the daemon's last background check.
    // Populates pendingUpdateCount so the UI can show a badge without
    // blocking on a fresh check.
    loadDaemonUpdateCache: qt_method!(fn loadDaemonUpdateCache(&mut self) {
        let path = daemon_cache_path();
        if let Ok(json) = std::fs::read_to_string(&path) {
            if let Ok(val) = serde_json::from_str::<serde_json::Value>(&json) {
                let count = val["total"].as_i64().unwrap_or(0) as i32;
                if self.pendingUpdateCount != count {
                    self.pendingUpdateCount = count;
                    self.pendingUpdateCountChanged();
                }
            }
        }
    }),

    // Update the local badge property and notify the tray daemon. This is used
    // after rows disappear so the tray badge tracks completed updates immediately.
    setPendingUpdateCount: qt_method!(fn setPendingUpdateCount(&mut self, count: i32) {
        let count = count.max(0);
        if self.pendingUpdateCount != count {
            self.pendingUpdateCount = count;
            self.pendingUpdateCountChanged();
        }

        let _ = std::fs::write(badge_count_path(), count.to_string());

        let cache_path = daemon_cache_path();
        if let Ok(json) = std::fs::read_to_string(&cache_path) {
            if let Ok(mut val) = serde_json::from_str::<serde_json::Value>(&json) {
                val["total"] = serde_json::json!(count);
                if count == 0 {
                    val["image_available"] = serde_json::json!(false);
                    val["reboot_required"] = serde_json::json!(false);
                }
                if let Some(parent) = cache_path.parent() {
                    let _ = std::fs::create_dir_all(parent);
                }
                let _ = std::fs::write(
                    &cache_path,
                    serde_json::to_string_pretty(&val).unwrap_or_default(),
                );
            }
        }
    }),

    // Return the raw JSON from the daemon's update cache, or "" if not yet
    // written (daemon still running its first check).
    loadUpdatesCache: qt_method!(fn loadUpdatesCache(&mut self) -> QString {
        std::fs::read_to_string(daemon_cache_path())
            .unwrap_or_default()
            .into()
    }),

    // Returns true if the daemon wrote a check-trigger flag (and consumes it).
    // The UI calls this on a timer and runs checkUpdates() when true.
    /// Returns true if a bootc upgrade or switch service is currently running
    /// under systemd. Call on updates page load to reconnect to an in-progress op.
    checkBootcOpActive: qt_method!(fn checkBootcOpActive(&mut self) -> bool {
        rakuos_updates::is_bootc_op_active()
    }),

    /// Reconnect to a running bootc upgrade — mirrors upgradeImage but skips the
    /// start step because the service is already active.
    reconnectBootcOp: qt_method!(fn reconnectBootcOp(&mut self) {
        self.start_op();
        let shared = self.get_shared();
        std::thread::spawn(move || {
            let mut exit_code = 1i32;
            for line in rakuos_updates::upgrade_stream() {
                if let Some(code) = line.strip_prefix("__done__") {
                    exit_code = code.trim().parse().unwrap_or(1);
                } else {
                    if let Some(pct) = parse_layers_progress(&line) {
                        shared.progress.store(pct, Ordering::Relaxed);
                    }
                    if !line.is_empty() { append_log(&line); }
                }
            }
            shared.result.store(if exit_code == 0 { 1 } else { 2 }, Ordering::Relaxed);
            shared.running.store(false, Ordering::Relaxed);
        });
    }),

    checkDaemonTrigger: qt_method!(fn checkDaemonTrigger(&mut self) -> bool {
        let path = std::env::temp_dir().join("rakuos-software-check-requested");
        if path.exists() {
            let _ = std::fs::remove_file(&path);
            true
        } else {
            false
        }
    }),

    // ── ODRS Reviews ──────────────────────────────────────────────────────────

    /// Start fetching ODRS reviews for app_id in a background thread.
    loadReviews: qt_method!(fn loadReviews(&mut self, app_id: QString) {
        let app_id = app_id.to_string();
        REVIEWS_RUNNING.store(true, Ordering::Relaxed);
        let _ = std::fs::write(reviews_log_path(), "[]");
        std::thread::spawn(move || {
            let rt = tokio::runtime::Runtime::new().unwrap();
            let reviews = rt.block_on(rakuos_reviews::get_reviews(&app_id));
            let json = serde_json::to_string(&reviews).unwrap_or_else(|_| "[]".into());
            let _ = std::fs::write(reviews_log_path(), json);
            REVIEWS_RUNNING.store(false, Ordering::Relaxed);
        });
    }),

    /// Returns true while reviews are being fetched.
    reviewsRunning: qt_method!(fn reviewsRunning(&mut self) -> bool {
        REVIEWS_RUNNING.load(Ordering::Relaxed)
    }),

    /// Read the cached reviews JSON (array of Review objects).
    readReviews: qt_method!(fn readReviews(&mut self) -> QString {
        std::fs::read_to_string(reviews_log_path())
            .unwrap_or_else(|_| "[]".into())
            .into()
    }),

    /// Submit a review asynchronously.  Poll reviewSubmitRunning(); when false
    /// call readReviewSubmitResult() for {"ok":bool,"msg":string}.
    submitReview: qt_method!(fn submitReview(
        &mut self,
        app_id: QString, summary: QString,
        description: QString, rating: i32, version: QString,
        display_name: QString
    ) {
        let app_id       = app_id.to_string();
        let summary      = summary.to_string();
        let description  = description.to_string();
        let version      = version.to_string();
        let display_name = display_name.to_string();
        let user_display: Option<String> = if display_name.is_empty() { None } else { Some(display_name) };
        REVIEW_SUBMIT_RUNNING.store(true, Ordering::Relaxed);
        let _ = std::fs::write(review_submit_log_path(), "");
        std::thread::spawn(move || {
            let rt = tokio::runtime::Runtime::new().unwrap();
            let result = rt.block_on(rakuos_reviews::submit_review(
                &app_id, &summary, &description, rating, &version, user_display.as_deref(),
            ));
            let json = match result {
                Ok(())   => serde_json::json!({"ok": true,  "msg": "Review submitted — thank you!"}).to_string(),
                Err(msg) => serde_json::json!({"ok": false, "msg": msg}).to_string(),
            };
            let _ = std::fs::write(review_submit_log_path(), json);
            REVIEW_SUBMIT_RUNNING.store(false, Ordering::Relaxed);
        });
    }),

    reviewSubmitRunning: qt_method!(fn reviewSubmitRunning(&mut self) -> bool {
        REVIEW_SUBMIT_RUNNING.load(Ordering::Relaxed)
    }),

    readReviewSubmitResult: qt_method!(fn readReviewSubmitResult(&mut self) -> QString {
        std::fs::read_to_string(review_submit_log_path())
            .unwrap_or_default()
            .into()
    }),

    /// Vote on a review — fire and forget.
    voteReview: qt_method!(fn voteReview(&mut self, review_id: i32, upvote: bool) {
        rakuos_reviews::vote_review(review_id as i64, upvote);
    }),
}

impl SoftwareBackend {
    fn get_shared(&mut self) -> Arc<SharedState> {
        if self.shared.is_none() {
            self.shared = Some(Arc::new(SharedState::default()));
        }
        self.shared.as_ref().unwrap().clone()
    }

    fn start_op(&mut self) {
        let s = self.get_shared();
        s.running.store(true, Ordering::Relaxed);
        s.result.store(0, Ordering::Relaxed);
        s.progress.store(2, Ordering::Relaxed);  // start at 2 so bar is immediately visible
        self.opRunning  = true;
        self.opResult   = 0;
        self.opProgress = 2;
        self.opStateChanged();
        let _ = std::fs::write(log_path(), "");
        self.logRevision = 0;
        self.logRevisionChanged();
    }

    /// Pop the next queued install/remove and start it, or clear banner if empty.
    fn dequeue_next_installop(&mut self) {
        if let Some(entry) = self.install_queue.pop_front() {
            let display = entry.app_name.clone();
            let is_remove = entry.is_remove;
            self.queueCount = self.install_queue.len() as i32;
            self.queueActiveName = display.into();
            self.queueActiveIconPath = entry.icon_path.clone().into();
            self.queueActiveIconUrl = entry.icon_url.clone().into();
            self.queueIsRemove = is_remove;
            self.queueStateChanged();
            if is_remove {
                self.removeApp(entry.id.into(), entry.source.into());
            } else {
                self.installApp(entry.id.into(), entry.source.into(), entry.remote.into());
            }
        } else {
            // Queue drained — hide banner.
            self.queueCount = 0;
            self.queueActiveName = QString::default();
            self.queueActiveIconPath = QString::default();
            self.queueActiveIconUrl = QString::default();
            self.queueIsRemove = false;
            self.queueStateChanged();
        }
    }
}

/// Returns (display_name, icon_path, icon_url) from the appstream cache.
fn lookup_app_info(id: &str) -> (String, String, String) {
    let cache = rakuos_appstream::get_appstream();
    if let Some(app) = cache.get(id) {
        (app.name.clone(), app.icon_path.clone(), app.icon_url.clone())
    } else {
        // Fallback: last dot-segment of the ID as a best-guess name
        let name = id.split('.').last().unwrap_or(id).to_string();
        (name, String::new(), String::new())
    }
}

fn lookup_app_icon(id: &str) -> (String, String) {
    let (_, ip, iu) = lookup_app_info(id);
    (ip, iu)
}

/// Parse [N/M] or "N of M" lines from dnf5/flatpak install output → 0-100.
fn parse_install_progress(line: &str) -> Option<i32> {
    let trimmed = line.trim();
    // [N/M] bracket format (dnf5, flatpak)
    if let Some(start) = trimmed.find('[') {
        if let Some(end) = trimmed[start..].find(']') {
            let inner = &trimmed[start + 1..start + end];
            if let Some(slash) = inner.find('/') {
                let n: f64 = inner[..slash].trim().parse().ok()?;
                let total: f64 = inner[slash + 1..].trim().parse().ok()?;
                if total > 0.0 {
                    return Some(((n / total) * 100.0).min(100.0) as i32);
                }
            }
        }
    }
    // "N of M" format
    if let Some(pos) = line.find(" of ") {
        let n_str = line[..pos].trim().split_whitespace().last().unwrap_or("");
        let m_str = line[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 i32);
            }
        }
    }
    None
}

fn parse_layers_progress(line: &str) -> Option<i32> {
    // Match various formats bootc/ostree/skopeo may emit:
    //   "layers[3/50]"   "layers [3/50]"   "[3/50]"   "3/50 layers"
    //   "Fetching 3 of 50"   "Pulling layer 3/50"
    let patterns: &[&str] = &[
        r"layers?\s*\[(\d+)/(\d+)\]",
        r"\[(\d+)/(\d+)\]",
        r"(\d+)\s*/\s*(\d+)\s+layers?",
        r"(\d+)\s+of\s+(\d+)",
        r"layer[s]?\s+(\d+)/(\d+)",
    ];
    for pat in patterns {
        if let Some(caps) = regex::Regex::new(pat).ok()?.captures(line) {
            let n: i32 = caps[1].parse().ok()?;
            let total: i32 = caps[2].parse().ok()?;
            if total > 0 { return Some((n * 100 / total).min(100)); }
        }
    }
    None
}
