#![allow(non_snake_case)]

use qmetaobject::prelude::*;
use std::sync::{
    atomic::{AtomicBool, AtomicI32, Ordering},
    Arc, Mutex,
};
use std::path::PathBuf;

const WELCOME_DONE_FILE: &str = ".config/rakuos/welcome-done";

pub fn done_file() -> PathBuf {
    let home = std::env::var("HOME").unwrap_or_else(|_| "/root".to_string());
    PathBuf::from(home).join(WELCOME_DONE_FILE)
}

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

fn load_apps_json() -> std::borrow::Cow<'static, str> {
    std::fs::read_to_string("/usr/share/rakuos-welcome/apps.json")
        .map(std::borrow::Cow::Owned)
        .unwrap_or(std::borrow::Cow::Borrowed(include_str!("../../../data/apps.json")))
}

fn check_connectivity() -> bool {
    std::process::Command::new("nmcli")
        .args(["networking", "connectivity", "check"])
        .output()
        .map(|o| {
            let s = String::from_utf8_lossy(&o.stdout);
            let t = s.trim();
            t == "full" || t == "limited"
        })
        .unwrap_or(false)
}

// ── Shared state between Qt thread and worker threads ────────────────────────

struct SharedState {
    install_running: AtomicBool,
    current_task:    AtomicI32,  // -1 = idle, 0..n = task index
    units_done:      AtomicI32,
    log_revision:    AtomicI32,
    wifi_scanning:   AtomicBool,
    wifi_connecting: AtomicBool,
}

impl Default for SharedState {
    fn default() -> Self {
        Self {
            install_running: AtomicBool::new(false),
            current_task:    AtomicI32::new(-1),
            units_done:      AtomicI32::new(0),
            log_revision:    AtomicI32::new(0),
            wifi_scanning:   AtomicBool::new(false),
            wifi_connecting: AtomicBool::new(false),
        }
    }
}

// ── QObject ──────────────────────────────────────────────────────────────────

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

    pub currentPage:       qt_property!(i32;     NOTIFY currentPageChanged),
    pub totalPages:        qt_property!(i32;     NOTIFY totalPagesChanged),
    pub scriptRunning:     qt_property!(bool;    NOTIFY scriptRunningChanged),
    pub installResult:     qt_property!(i32;     NOTIFY installResultChanged),
    pub currentTask:       qt_property!(i32;     NOTIFY currentTaskChanged),
    pub logRevision:       qt_property!(i32;     NOTIFY logRevisionChanged),
    pub catalogJson:       qt_property!(QString; NOTIFY catalogJsonChanged),
    pub installedJson:     qt_property!(QString; NOTIFY installedJsonChanged),
    pub forceMode:         qt_property!(bool;    NOTIFY forceModeChanged),
    pub wifiScanning:      qt_property!(bool;    NOTIFY wifiScanningChanged),
    pub wifiConnecting:    qt_property!(bool;    NOTIFY wifiConnectingChanged),
    pub wifiNetworksJson:  qt_property!(QString; NOTIFY wifiNetworksJsonChanged),
    pub wifiStatusMsg:     qt_property!(QString; NOTIFY wifiStatusMsgChanged),
    pub hasNetwork:        qt_property!(bool;    NOTIFY hasNetworkChanged),
    pub taskProgressJson:  qt_property!(QString; NOTIFY taskProgressJsonChanged),
    pub totalUnits:        qt_property!(i32;     NOTIFY totalUnitsChanged),
    pub unitsDone:         qt_property!(i32;     NOTIFY unitsDoneChanged),
    pub languagesJson:     qt_property!(QString; NOTIFY languagesJsonChanged),
    pub keyboardsJson:     qt_property!(QString; NOTIFY keyboardsJsonChanged),
    pub citiesJson:        qt_property!(QString; NOTIFY citiesJsonChanged),

    pub currentPageChanged:       qt_signal!(),
    pub totalPagesChanged:        qt_signal!(),
    pub scriptRunningChanged:     qt_signal!(),
    pub installResultChanged:     qt_signal!(),
    pub currentTaskChanged:       qt_signal!(),
    pub logRevisionChanged:       qt_signal!(),
    pub catalogJsonChanged:       qt_signal!(),
    pub installedJsonChanged:     qt_signal!(),
    pub forceModeChanged:         qt_signal!(),
    pub wifiScanningChanged:      qt_signal!(),
    pub wifiConnectingChanged:    qt_signal!(),
    pub wifiNetworksJsonChanged:  qt_signal!(),
    pub wifiStatusMsgChanged:     qt_signal!(),
    pub hasNetworkChanged:        qt_signal!(),
    pub taskProgressJsonChanged:  qt_signal!(),
    pub totalUnitsChanged:        qt_signal!(),
    pub unitsDoneChanged:         qt_signal!(),
    pub languagesJsonChanged:     qt_signal!(),
    pub keyboardsJsonChanged:     qt_signal!(),
    pub citiesJsonChanged:        qt_signal!(),
    pub closeRequested:           qt_signal!(),

    // ── initCatalog ──────────────────────────────────────────────────────────
    pub initCatalog: qt_method!(fn initCatalog(&mut self) {
        let force = std::env::args_os().any(|a| a == "--force" || a == "-f");
        self.forceMode = force;
        self.forceModeChanged();

        let json = load_apps_json();
        let v: serde_json::Value = serde_json::from_str(&json).unwrap_or_default();

        let n: i32 = v["categories"].as_array().map(|a| a.len() as i32).unwrap_or(0);
        // Pages: 0=welcome  1..n=categories  n+1=language  n+2=keyboard  n+3=location
        //        n+4=network(conditional)  n+5=install  n+6=done
        self.totalPages = n + 7;
        self.totalPagesChanged();

        self.catalogJson = QString::from(json.as_ref());
        self.catalogJsonChanged();

        let online = check_connectivity();
        self.hasNetwork = online;
        self.hasNetworkChanged();

        let installed_rpms: std::collections::HashSet<String> = std::process::Command::new("rpm")
            .args(["-qa", "--qf", "%{NAME}\\n"])
            .output()
            .map(|o| String::from_utf8_lossy(&o.stdout).lines().map(|s| s.to_string()).collect())
            .unwrap_or_default();

        let installed_flatpaks: std::collections::HashSet<String> = std::process::Command::new("flatpak")
            .args(["list", "--app", "--columns=application"])
            .output()
            .map(|o| String::from_utf8_lossy(&o.stdout).lines().map(|s| s.to_string()).collect())
            .unwrap_or_default();

        let mut installed_map = serde_json::Map::new();
        if let Some(cats) = v["categories"].as_array() {
            for cat in cats {
                if let Some(apps) = cat["apps"].as_array() {
                    for app in apps {
                        if let (Some(id), Some(kind), Some(pkg)) = (
                            app["id"].as_str(),
                            app["type"].as_str(),
                            app["package"].as_str(),
                        ) {
                            let installed = match kind {
                                "flatpak" => installed_flatpaks.contains(pkg),
                                "script"  => false,
                                _         => installed_rpms.contains(pkg),
                            };
                            installed_map.insert(id.to_string(), serde_json::Value::Bool(installed));
                        }
                    }
                }
            }
        }
        let installed_str = serde_json::to_string(&serde_json::Value::Object(installed_map))
            .unwrap_or_else(|_| "{}".to_string());
        self.installedJson = QString::from(installed_str.as_str());
        self.installedJsonChanged();
    }),

    // ── nextPage ─────────────────────────────────────────────────────────────
    pub nextPage: qt_method!(fn nextPage(&mut self) {
        let total = self.totalPages;
        let idx   = self.currentPage;
        if idx >= total - 1 { return; }

        // Derived page indices
        let last_cat     = total - 7;
        let language_idx = total - 6;
        let keyboard_idx = total - 5;
        let location_idx = total - 4;
        let network_idx  = total - 3;
        let install_idx  = total - 2;

        // Last category → skip setup pages when --force (unless test env is set)
        if idx == last_cat {
            let skip_setup = self.forceMode && std::env::var("RAKUOS_WELCOME_TEST_SETUP").is_err();
            if skip_setup {
                self.currentPage = install_idx;
            } else {
                self.currentPage = language_idx;
            }
            self.currentPageChanged();
            return;
        }

        // Language → keyboard (apply locale in background)
        if idx == language_idx {
            self.currentPage = keyboard_idx;
            self.currentPageChanged();
            return;
        }

        // Keyboard → location (apply keyboard in background)
        if idx == keyboard_idx {
            self.currentPage = location_idx;
            self.currentPageChanged();
            return;
        }

        // Location → network or install (apply timezone in background)
        if idx == location_idx {
            let online = check_connectivity();
            self.hasNetwork = online;
            self.hasNetworkChanged();
            self.currentPage = if online { install_idx } else { network_idx };
            self.currentPageChanged();
            return;
        }

        // Network → install
        if idx == network_idx {
            self.currentPage = install_idx;
            self.currentPageChanged();
            return;
        }

        self.shared.log_revision.store(0, Ordering::Relaxed);
        self.installResult = 0;
        self.installResultChanged();
        self.scriptRunning = false;
        self.scriptRunningChanged();
        self.currentPage = idx + 1;
        self.currentPageChanged();
    }),

    // ── backPage ─────────────────────────────────────────────────────────────
    pub backPage: qt_method!(fn backPage(&mut self) {
        let idx   = self.currentPage;
        let total = self.totalPages;
        if idx <= 0 { return; }

        self.shared.log_revision.store(0, Ordering::Relaxed);
        self.installResult = 0;
        self.installResultChanged();
        self.scriptRunning = false;
        self.scriptRunningChanged();

        let last_cat     = total - 7;
        let location_idx = total - 4;
        let network_idx  = total - 3;
        let install_idx  = total - 2;

        // From install page
        if idx == install_idx {
            if self.forceMode && std::env::var("RAKUOS_WELCOME_TEST_SETUP").is_err() {
                self.currentPage = last_cat;
            } else if self.hasNetwork {
                // had network, skipped network page → go back to location
                self.currentPage = location_idx;
            } else {
                self.currentPage = network_idx;
            }
            self.currentPageChanged();
            return;
        }

        // From network page → back to location
        if idx == network_idx {
            self.currentPage = location_idx;
            self.currentPageChanged();
            return;
        }

        self.currentPage = idx - 1;
        self.currentPageChanged();
    }),

    // ── pollScript ───────────────────────────────────────────────────────────
    pub pollScript: qt_method!(fn pollScript(&mut self) {
        let new_running  = self.shared.install_running.load(Ordering::Relaxed);
        let new_task     = self.shared.current_task.load(Ordering::Relaxed);
        let new_rev      = self.shared.log_revision.load(Ordering::Relaxed);
        let new_ud       = self.shared.units_done.load(Ordering::Relaxed);
        let new_scanning = self.shared.wifi_scanning.load(Ordering::Relaxed);
        let new_conn     = self.shared.wifi_connecting.load(Ordering::Relaxed);

        if self.scriptRunning != new_running {
            self.scriptRunning = new_running;
            self.scriptRunningChanged();
            if !new_running && self.installResult == 0 {
                self.installResult = 1;
                self.installResultChanged();
                // Rebuild taskProgressJson from state
                let arr = {
                    let p = self.task_progress_state.lock().unwrap();
                    let v: Vec<serde_json::Value> = p.iter()
                        .map(|(name, weight, remove, status)| serde_json::json!({
                            "name": name, "weight": weight, "remove": remove, "status": status
                        }))
                        .collect();
                    serde_json::to_string(&v).unwrap_or_else(|_| "[]".to_string())
                };
                self.taskProgressJson = QString::from(arr.as_str());
                self.taskProgressJsonChanged();
            }
        }
        if self.currentTask != new_task {
            self.currentTask = new_task;
            self.currentTaskChanged();
            // Rebuild taskProgressJson
            let arr = {
                let p = self.task_progress_state.lock().unwrap();
                let v: Vec<serde_json::Value> = p.iter()
                    .map(|(name, weight, remove, status)| serde_json::json!({
                        "name": name, "weight": weight, "remove": remove, "status": status
                    }))
                    .collect();
                serde_json::to_string(&v).unwrap_or_else(|_| "[]".to_string())
            };
            self.taskProgressJson = QString::from(arr.as_str());
            self.taskProgressJsonChanged();
        }
        if self.unitsDone != new_ud {
            self.unitsDone = new_ud;
            self.unitsDoneChanged();
        }
        if self.logRevision != new_rev {
            self.logRevision = new_rev;
            self.logRevisionChanged();
        }
        if self.wifiScanning != new_scanning {
            self.wifiScanning = new_scanning;
            self.wifiScanningChanged();
            if !new_scanning {
                let results = self.wifi_scan_results.lock().unwrap().clone().unwrap_or_default();
                let arr: Vec<serde_json::Value> = results.iter()
                    .map(|(ssid, signal, secured)| serde_json::json!({
                        "ssid": ssid, "signal": signal, "secured": secured
                    }))
                    .collect();
                let json = serde_json::to_string(&arr).unwrap_or_else(|_| "[]".to_string());
                self.wifiNetworksJson = QString::from(json.as_str());
                self.wifiNetworksJsonChanged();
                self.wifiStatusMsg = QString::from("Scan complete.");
                self.wifiStatusMsgChanged();
            }
        }
        if self.wifiConnecting != new_conn {
            self.wifiConnecting = new_conn;
            self.wifiConnectingChanged();
            if !new_conn {
                let ok = self.wifi_connect_result.lock().unwrap().unwrap_or(false);
                if ok {
                    let online = check_connectivity();
                    self.hasNetwork = online;
                    self.hasNetworkChanged();
                    let msg = if online { "Connected!" } else { "Connected but no internet." };
                    self.wifiStatusMsg = QString::from(msg);
                } else {
                    self.wifiStatusMsg = QString::from("Connection failed. Check the password.");
                }
                self.wifiStatusMsgChanged();
            }
        }
    }),

    // ── scanWifi ─────────────────────────────────────────────────────────────
    pub scanWifi: qt_method!(fn scanWifi(&mut self) {
        if self.shared.wifi_scanning.load(Ordering::Relaxed) { return; }
        self.shared.wifi_scanning.store(true, Ordering::Relaxed);
        self.wifiScanning = true;
        self.wifiScanningChanged();
        self.wifiStatusMsg = QString::from("Scanning for networks…");
        self.wifiStatusMsgChanged();

        let shared  = self.shared.clone();
        let results = self.wifi_scan_results.clone();

        std::thread::spawn(move || {
            let output = std::process::Command::new("nmcli")
                .args(["-t", "-f", "SSID,SIGNAL,SECURITY", "device", "wifi", "list", "--rescan", "yes"])
                .output();

            let mut networks: Vec<(String, u8, bool)> = vec![];
            if let Ok(o) = output {
                for line in String::from_utf8_lossy(&o.stdout).lines() {
                    let parts: Vec<&str> = line.splitn(3, ':').collect();
                    if parts.len() >= 2 {
                        let ssid = parts[0].to_string();
                        if ssid.is_empty() { continue; }
                        let signal: u8 = parts[1].parse().unwrap_or(0);
                        let secured = parts.get(2).map(|s| !s.trim().is_empty()).unwrap_or(false);
                        networks.push((ssid, signal, secured));
                    }
                }
            }
            // Deduplicate: keep strongest signal per SSID
            networks.sort_by(|a, b| b.1.cmp(&a.1));
            let mut seen = std::collections::HashSet::new();
            networks.retain(|(ssid, _, _)| seen.insert(ssid.clone()));

            *results.lock().unwrap() = Some(networks);
            shared.wifi_scanning.store(false, Ordering::Relaxed);
        });
    }),

    // ── connectWifi ──────────────────────────────────────────────────────────
    pub connectWifi: qt_method!(fn connectWifi(&mut self, ssid: QString, password: QString) {
        if self.shared.wifi_connecting.load(Ordering::Relaxed) { return; }
        let ssid_s = ssid.to_string();
        let pass_s = password.to_string();
        if ssid_s.is_empty() { return; }

        self.shared.wifi_connecting.store(true, Ordering::Relaxed);
        self.wifiConnecting = true;
        self.wifiConnectingChanged();
        let msg = format!("Connecting to {}…", ssid_s);
        self.wifiStatusMsg = QString::from(msg.as_str());
        self.wifiStatusMsgChanged();

        let shared = self.shared.clone();
        let result = self.wifi_connect_result.clone();

        std::thread::spawn(move || {
            let ok = if pass_s.is_empty() {
                std::process::Command::new("nmcli")
                    .args(["device", "wifi", "connect", &ssid_s])
                    .status()
                    .map(|s| s.success())
                    .unwrap_or(false)
            } else {
                std::process::Command::new("nmcli")
                    .args(["device", "wifi", "connect", &ssid_s, "password", &pass_s])
                    .status()
                    .map(|s| s.success())
                    .unwrap_or(false)
            };
            *result.lock().unwrap() = Some(ok);
            shared.wifi_connecting.store(false, Ordering::Relaxed);
        });
    }),

    // ── installAll ───────────────────────────────────────────────────────────
    /// selectionsJson: {"app_id": bool, …} across all categories.
    /// removeJson: [{name,type,package,…}] for exclusive-category uninstalls.
    pub installAll: qt_method!(fn installAll(&mut self, selectionsJson: QString, removeJson: QString) {
        if self.shared.install_running.load(Ordering::Relaxed) { return; }

        let selections: std::collections::HashMap<String, bool> =
            serde_json::from_str(&selectionsJson.to_string()).unwrap_or_default();
        let remove_apps: Vec<serde_json::Value> =
            serde_json::from_str(&removeJson.to_string()).unwrap_or_default();

        let json = load_apps_json();
        let v: serde_json::Value = serde_json::from_str(&json).unwrap_or_default();

        // Build ordered (name, kind, pkg, remote, extras, weight, is_remove)
        type Task = (String, String, String, String, Vec<String>, usize, bool);
        let mut tasks: Vec<Task> = vec![];

        for app in &remove_apps {
            let name  = app["name"].as_str().unwrap_or("app").to_string();
            let kind  = app["type"].as_str().unwrap_or("rpm").to_string();
            let pkg   = app["package"].as_str().unwrap_or("").to_string();
            tasks.push((name, kind, pkg, String::new(), vec![], 1, true));
        }

        if let Some(cats) = v["categories"].as_array() {
            for cat in cats {
                if let Some(apps) = cat["apps"].as_array() {
                    for app in apps {
                        let id = app["id"].as_str().unwrap_or("").to_string();
                        if *selections.get(&id).unwrap_or(&false) {
                            let name   = app["name"].as_str().unwrap_or(&id).to_string();
                            let kind   = app["type"].as_str().unwrap_or("rpm").to_string();
                            let pkg    = app["package"].as_str().unwrap_or("").to_string();
                            let remote = app["remote"].as_str().unwrap_or("flathub").to_string();
                            let extras: Vec<String> = app["packages"]
                                .as_array()
                                .map(|a| a.iter().filter_map(|v| v.as_str().map(|s| s.to_string())).collect())
                                .unwrap_or_default();
                            let weight = 1 + extras.len();
                            tasks.push((name, kind, pkg, remote, extras, weight, false));
                        }
                    }
                }
            }
        }

        if tasks.is_empty() { return; }

        let total_units: i32 = tasks.iter().map(|(_, _, _, _, _, w, _)| *w as i32).sum();
        self.totalUnits = total_units;
        self.totalUnitsChanged();
        self.unitsDone = 0;
        self.unitsDoneChanged();

        // Initialise task_progress_state
        {
            let mut p = self.task_progress_state.lock().unwrap();
            *p = tasks.iter()
                .map(|(name, _, _, _, extras, weight, remove)| (name.clone(), *weight, *remove, 0u8))
                .collect();
        }
        let init_json = {
            let p = self.task_progress_state.lock().unwrap();
            let v: Vec<serde_json::Value> = p.iter()
                .map(|(name, weight, remove, status)| serde_json::json!({
                    "name": name, "weight": weight, "remove": remove, "status": status
                }))
                .collect();
            serde_json::to_string(&v).unwrap_or_else(|_| "[]".to_string())
        };
        self.taskProgressJson = QString::from(init_json.as_str());
        self.taskProgressJsonChanged();

        let log_path = log_file_path();
        let _ = std::fs::write(&log_path, "Starting installation…\n");

        self.shared.install_running.store(true, Ordering::Relaxed);
        self.shared.current_task.store(0, Ordering::Relaxed);
        self.shared.units_done.store(0, Ordering::Relaxed);
        self.shared.log_revision.store(1, Ordering::Relaxed);
        self.installResult = 0;
        self.installResultChanged();
        self.scriptRunning = true;
        self.scriptRunningChanged();
        self.currentTask = 0;
        self.currentTaskChanged();

        let shared   = self.shared.clone();
        let progress = self.task_progress_state.clone();

        std::thread::spawn(move || {
            use std::io::{BufRead, BufReader, Write};
            use std::process::{Command, Stdio};

            let mut units_done: i32 = 0;
            let mut rev = 1i32;

            let append = |text: &str, rev: &mut i32| {
                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");
                }
                *rev += 1;
                shared.log_revision.store(*rev, Ordering::Relaxed);
            };

            let run_cmd = |cmd: &str, args: Vec<String>, append: &mut dyn FnMut(&str)| -> bool {
                match Command::new(cmd)
                    .args(&args)
                    .stdout(Stdio::piped())
                    .stderr(Stdio::piped())
                    .spawn()
                {
                    Ok(mut child) => {
                        if let Some(out) = child.stdout.take() {
                            for line in BufReader::new(out).lines().map_while(Result::ok) {
                                if !line.is_empty() { append(&line); }
                            }
                        }
                        if let Some(err) = child.stderr.take() {
                            for line in BufReader::new(err).lines().map_while(Result::ok) {
                                if !line.is_empty() { append(&line); }
                            }
                        }
                        child.wait().map(|s| s.success()).unwrap_or(false)
                    }
                    Err(e) => { append(&e.to_string()); false }
                }
            };

            for (i, (name, kind, pkg, remote, extras, weight, is_remove)) in tasks.iter().enumerate() {
                shared.current_task.store(i as i32, Ordering::Relaxed);
                // Mark as running (status = 1)
                { let mut p = progress.lock().unwrap(); if let Some(e) = p.get_mut(i) { e.3 = 1; } }

                let verb = if *is_remove { "Removing" } else { "Installing" };
                let msg = format!("→ {} {}…", verb, name);
                append(&msg, &mut rev);

                let ok = if *is_remove {
                    if kind == "flatpak" {
                        run_cmd("flatpak",
                            vec!["uninstall".into(), "--user".into(), "-y".into(), pkg.clone()],
                            &mut |l| append(l, &mut rev))
                    } else {
                        run_cmd("pkexec",
                            vec!["/usr/libexec/rakuos/rakuos-remove".into(), pkg.clone()],
                            &mut |l| append(l, &mut rev))
                    }
                } else {
                    match kind.as_str() {
                        "flatpak" => {
                            let mut args = vec!["install".into(), "--user".into(),
                                "--noninteractive".into(), "-y".into(), remote.clone(), pkg.clone()];
                            args.extend(extras.clone());
                            run_cmd("flatpak", args, &mut |l| append(l, &mut rev))
                        }
                        "script" => run_cmd("pkexec",
                            vec![format!("/usr/libexec/rakuos/{}", pkg)],
                            &mut |l| append(l, &mut rev)),
                        _ => {
                            let mut args = vec!["/usr/libexec/rakuos/rakuos-install".into(), pkg.clone()];
                            args.extend(extras.clone());
                            run_cmd("pkexec", args, &mut |l| append(l, &mut rev))
                        }
                    }
                };

                // Mark done (status = 2) or failed (status = 3)
                { let mut p = progress.lock().unwrap(); if let Some(e) = p.get_mut(i) { e.3 = if ok { 2 } else { 3 }; } }

                units_done += *weight as i32;
                shared.units_done.store(units_done, Ordering::Relaxed);

                let result_msg = if ok {
                    format!("✓ {} done", name)
                } else {
                    format!("✗ {} failed", name)
                };
                append(&result_msg, &mut rev);
            }

            append("Installation complete.", &mut rev);
            shared.install_running.store(false, Ordering::Relaxed);
            shared.current_task.store(tasks.len() as i32, Ordering::Relaxed);
        });
    }),

    // ── Setup page data loaders ───────────────────────────────────────────────

    pub loadLanguages: qt_method!(fn loadLanguages(&mut self) {
        let locales = welcome_setup::list_locales();
        let json = serde_json::to_string(&locales).unwrap_or_else(|_| "[]".to_string());
        self.languagesJson = QString::from(json.as_str());
        self.languagesJsonChanged();
    }),

    pub loadKeyboards: qt_method!(fn loadKeyboards(&mut self) {
        let entries = welcome_setup::keyboard_layout_entries();
        let arr: Vec<serde_json::Value> = entries.iter().map(|e| serde_json::json!({
            "layout": e.layout,
            "variant": e.variant,
            "description": e.description,
        })).collect();
        let json = serde_json::to_string(&arr).unwrap_or_else(|_| "[]".to_string());
        self.keyboardsJson = QString::from(json.as_str());
        self.keyboardsJsonChanged();
    }),

    pub searchCities: qt_method!(fn searchCities(&mut self, query: QString) {
        let q = query.to_string().to_lowercase();
        if q.is_empty() {
            self.citiesJson = QString::from("[]");
            self.citiesJsonChanged();
            return;
        }
        let cities = welcome_setup::load_cities();
        let arr: Vec<serde_json::Value> = cities.iter()
            .filter(|c| {
                c.name.to_lowercase().contains(&q)
                    || c.alternate_names.iter().any(|n| n.to_lowercase().contains(&q))
            })
            .take(100)
            .map(|c| serde_json::json!({ "name": c.name, "timezone": c.timezone }))
            .collect();
        let json = serde_json::to_string(&arr).unwrap_or_else(|_| "[]".to_string());
        self.citiesJson = QString::from(json.as_str());
        self.citiesJsonChanged();
    }),

    pub applyLocale: qt_method!(fn applyLocale(&mut self, locale: QString) {
        let s = locale.to_string();
        std::thread::spawn(move || welcome_setup::apply_locale(&s));
    }),

    pub applyKeyboard: qt_method!(fn applyKeyboard(&mut self, layout: QString, variant: QString) {
        let l = layout.to_string();
        let v = variant.to_string();
        std::thread::spawn(move || welcome_setup::apply_keyboard(&l, &v));
    }),

    pub applyTimezone: qt_method!(fn applyTimezone(&mut self, tz: QString) {
        let s = tz.to_string();
        std::thread::spawn(move || welcome_setup::apply_timezone(&s));
    }),

    /// Returns a file:// URL for the app icon, or "" for QML theme fallback.
    pub resolveIcon: qt_method!(fn resolveIcon(&self, kind: QString, app_id: QString, icon_name: QString) -> QString {
        let (path, url) = if let Some(bundled) = rakuos_appstream::bundled_icon_path(&app_id.to_string()) {
            (bundled, String::new())
        } else {
            rakuos_appstream::resolve_icon_for_package(
                &kind.to_string(),
                &app_id.to_string(),
                &icon_name.to_string(),
            )
        };
        if !path.is_empty() {
            return QString::from(format!("file://{}", path));
        }
        if !url.is_empty() {
            return QString::from(url);
        }
        QString::from("")
    }),

    pub finishSetup: qt_method!(fn finishSetup(&mut self) {
        let df = done_file();
        let _ = std::fs::create_dir_all(df.parent().unwrap());
        let _ = std::fs::File::create(&df);
        self.closeRequested();
    }),

    // ── Private fields ───────────────────────────────────────────────────────
    shared:              Arc<SharedState>,
    wifi_scan_results:   Arc<Mutex<Option<Vec<(String, u8, bool)>>>>,
    wifi_connect_result: Arc<Mutex<Option<bool>>>,
    /// (name, weight, is_remove, status:0=pending/1=running/2=ok/3=fail)
    task_progress_state: Arc<Mutex<Vec<(String, usize, bool, u8)>>>,
}

impl Default for WelcomeBackend {
    fn default() -> Self {
        WelcomeBackend {
            base:                    Default::default(),
            currentPage:             Default::default(),
            totalPages:              Default::default(),
            scriptRunning:           Default::default(),
            installResult:           Default::default(),
            currentTask:             Default::default(),
            logRevision:             Default::default(),
            catalogJson:             Default::default(),
            installedJson:           Default::default(),
            forceMode:               Default::default(),
            wifiScanning:            Default::default(),
            wifiConnecting:          Default::default(),
            wifiNetworksJson:        QString::from("[]"),
            wifiStatusMsg:           Default::default(),
            hasNetwork:              Default::default(),
            taskProgressJson:        QString::from("[]"),
            totalUnits:              Default::default(),
            unitsDone:               Default::default(),
            languagesJson:           QString::from("[]"),
            keyboardsJson:           QString::from("[]"),
            citiesJson:              QString::from("[]"),
            currentPageChanged:      Default::default(),
            totalPagesChanged:       Default::default(),
            scriptRunningChanged:    Default::default(),
            installResultChanged:    Default::default(),
            currentTaskChanged:      Default::default(),
            logRevisionChanged:      Default::default(),
            catalogJsonChanged:      Default::default(),
            installedJsonChanged:    Default::default(),
            forceModeChanged:        Default::default(),
            wifiScanningChanged:     Default::default(),
            wifiConnectingChanged:   Default::default(),
            wifiNetworksJsonChanged: Default::default(),
            wifiStatusMsgChanged:    Default::default(),
            hasNetworkChanged:       Default::default(),
            taskProgressJsonChanged: Default::default(),
            totalUnitsChanged:       Default::default(),
            unitsDoneChanged:        Default::default(),
            languagesJsonChanged:    Default::default(),
            keyboardsJsonChanged:    Default::default(),
            citiesJsonChanged:       Default::default(),
            closeRequested:          Default::default(),
            initCatalog:             Default::default(),
            nextPage:                Default::default(),
            backPage:                Default::default(),
            pollScript:              Default::default(),
            scanWifi:                Default::default(),
            connectWifi:             Default::default(),
            installAll:              Default::default(),
            loadLanguages:           Default::default(),
            loadKeyboards:           Default::default(),
            searchCities:            Default::default(),
            applyLocale:             Default::default(),
            applyKeyboard:           Default::default(),
            applyTimezone:           Default::default(),
            resolveIcon:             Default::default(),
            finishSetup:             Default::default(),
            shared:                  Arc::new(SharedState::default()),
            wifi_scan_results:       Arc::new(Mutex::new(None)),
            wifi_connect_result:     Arc::new(Mutex::new(None)),
            task_progress_state:     Arc::new(Mutex::new(vec![])),
        }
    }
}
