use std::collections::{HashMap, HashSet};
use std::io::{BufRead, BufReader};
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::sync::{
    atomic::{AtomicBool, AtomicI32, Ordering},
    Arc, Mutex,
};
use std::thread;
use std::time::Duration;

use gtk4::glib;
use gtk4::prelude::*;
use gtk4::{
    Align, Box as GtkBox, Entry, FlowBox, IconTheme, Label, ListBox,
    Orientation, PolicyType, ProgressBar, ScrolledWindow, SelectionMode,
    Separator, Stack, StackTransitionType, TextView, ToggleButton, WrapMode,
};
use libadwaita::prelude::*;
use libadwaita::{Application, ApplicationWindow, Avatar, HeaderBar, StyleManager};
use serde::Deserialize;

const APP_ID: &str = "org.rakuos.Welcome";
const WELCOME_DONE_FILE: &str = ".config/rakuos/welcome-done";
const LOGO_DARK: &str = "/usr/share/pixmaps/fedora_whitelogo_med.png";
const LOGO_LIGHT: &str = "/usr/share/pixmaps/fedora_logo_med.png";

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")))
}

// ── Catalog types ─────────────────────────────────────────────────────────────

#[derive(Debug, Deserialize, Clone)]
struct AppEntry {
    id: String,
    name: String,
    summary: String,
    #[serde(default)]
    icon: String,
    #[serde(rename = "type")]
    kind: String,
    package: String,
    #[serde(default)]
    remote: String,
    #[serde(default)]
    packages: Vec<String>,
    #[serde(skip)]
    icon_path: String,
    #[serde(skip)]
    icon_url: String,
}

impl AppEntry {
    fn unit_weight(&self) -> usize { 1 + self.packages.len() }
}

#[derive(Debug, Deserialize, Clone)]
struct Category {
    id: String,
    title: String,
    description: String,
    apps: Vec<AppEntry>,
    #[serde(default)]
    exclusive: bool,
}

#[derive(Debug, Deserialize)]
struct Catalog { categories: Vec<Category> }

// ── WiFi types ────────────────────────────────────────────────────────────────

#[derive(Clone)]
struct WifiNetwork { ssid: String, signal: u8, secured: bool }

// ── Worker messages ───────────────────────────────────────────────────────────

enum WorkerMsg { Line(String), Done }

// ── Icon widget ───────────────────────────────────────────────────────────────

fn make_icon_widget(icon_path: &str, icon_url: &str, size: i32, name: &str) -> gtk4::Widget {
    if !icon_path.is_empty() && Path::new(icon_path).exists() {
        let img = gtk4::Image::builder().file(icon_path).pixel_size(size).build();
        img.set_halign(Align::Center);
        return img.upcast();
    }
    if !icon_url.is_empty() {
        let img = gtk4::Image::builder().icon_name("package-x-generic").pixel_size(size).build();
        img.set_halign(Align::Center);
        let url = icon_url.to_string();
        let (tx, rx) = std::sync::mpsc::channel::<Option<Vec<u8>>>();
        thread::spawn(move || {
            let result = Command::new("curl").args(["-sL", "--max-time", "10", &url])
                .output().ok()
                .and_then(|o| if o.status.success() { Some(o.stdout) } else { None });
            let _ = tx.send(result);
        });
        let img_c = img.clone();
        glib::timeout_add_local(Duration::from_millis(100), move || {
            match rx.try_recv() {
                Ok(Some(bytes)) => {
                    let loader = gtk4::gdk_pixbuf::PixbufLoader::new();
                    if loader.write(&bytes).is_ok() && loader.close().is_ok() {
                        if let Some(pb) = loader.pixbuf() {
                            if let Some(scaled) = pb.scale_simple(size, size, gtk4::gdk_pixbuf::InterpType::Bilinear) {
                                img_c.set_from_pixbuf(Some(&scaled));
                            }
                        }
                    }
                    glib::ControlFlow::Break
                }
                Ok(None) => glib::ControlFlow::Break,
                Err(std::sync::mpsc::TryRecvError::Empty) => glib::ControlFlow::Continue,
                Err(_) => glib::ControlFlow::Break,
            }
        });
        return img.upcast();
    }
    let initials: String = name.split_whitespace().take(2).filter_map(|w| w.chars().next()).collect();
    Avatar::builder().text(if initials.is_empty() { name } else { &initials })
        .size(size).show_initials(true).build().upcast()
}

// ── Installed-package detection ───────────────────────────────────────────────

fn query_installed_rpms() -> HashSet<String> {
    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()
}

fn query_installed_flatpaks() -> HashSet<String> {
    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()
}

fn is_installed(app: &AppEntry, rpms: &HashSet<String>, flatpaks: &HashSet<String>) -> bool {
    match app.kind.as_str() {
        "flatpak" => flatpaks.contains(&app.package),
        "script"  => false,
        _         => rpms.contains(&app.package),
    }
}

// ── Network helpers ───────────────────────────────────────────────────────────

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

fn scan_wifi() -> Vec<WifiNetwork> {
    let out = match Command::new("nmcli")
        .args(["-t", "-f", "SSID,SIGNAL,SECURITY", "device", "wifi", "list", "--rescan", "yes"])
        .output()
    {
        Ok(o) => o,
        Err(_) => return vec![],
    };
    let mut seen = HashSet::new();
    String::from_utf8_lossy(&out.stdout).lines()
        .filter_map(|line| {
            let parts: Vec<&str> = line.splitn(3, ':').collect();
            let ssid = parts.first().map(|s| s.replace("\\:", ":")).unwrap_or_default();
            if ssid.is_empty() || !seen.insert(ssid.clone()) { return None; }
            let signal: u8 = parts.get(1).and_then(|s| s.parse().ok()).unwrap_or(0);
            let secured = parts.get(2).map_or(false, |s| !s.is_empty() && *s != "--");
            Some(WifiNetwork { ssid, signal, secured })
        })
        .collect()
}

fn connect_wifi(ssid: &str, password: &str) -> bool {
    let mut cmd = Command::new("nmcli");
    cmd.args(["device", "wifi", "connect", ssid]);
    if !password.is_empty() { cmd.args(["password", password]); }
    cmd.status().map(|s| s.success()).unwrap_or(false)
}

// ── Helpers ───────────────────────────────────────────────────────────────────

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

// ── Install runners ───────────────────────────────────────────────────────────

fn run_remove(app: &AppEntry, sender: &async_channel::Sender<WorkerMsg>) -> bool {
    match app.kind.as_str() {
        "flatpak" => spawn_and_stream("flatpak", &["uninstall", "--user", "-y", &app.package], sender),
        _         => spawn_and_stream("pkexec", &["/usr/libexec/rakuos/rakuos-remove", &app.package], sender),
    }
}

fn run_flatpak(app: &AppEntry, sender: &async_channel::Sender<WorkerMsg>) -> bool {
    let remote = if app.remote.is_empty() { "flathub" } else { &app.remote };
    let mut args = vec!["install", "--user", "--noninteractive", "-y", remote, &app.package];
    let extras: Vec<&str> = app.packages.iter().map(|s| s.as_str()).collect();
    args.extend_from_slice(&extras);
    spawn_and_stream("flatpak", &args, sender)
}

fn run_rpm(app: &AppEntry, sender: &async_channel::Sender<WorkerMsg>) -> bool {
    let mut args = vec!["/usr/libexec/rakuos/rakuos-install", &app.package];
    let extras: Vec<&str> = app.packages.iter().map(|s| s.as_str()).collect();
    args.extend_from_slice(&extras);
    spawn_and_stream("pkexec", &args, sender)
}

fn run_script(script: &str, sender: &async_channel::Sender<WorkerMsg>) -> bool {
    let path = format!("/usr/libexec/rakuos/{}", script);
    spawn_and_stream("pkexec", &[path.as_str()], sender)
}

fn spawn_and_stream(cmd: &str, args: &[&str], sender: &async_channel::Sender<WorkerMsg>) -> 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() { let _ = sender.send_blocking(WorkerMsg::Line(line)); }
                }
            }
            if let Some(err) = child.stderr.take() {
                for line in BufReader::new(err).lines().map_while(Result::ok) {
                    if !line.is_empty() { let _ = sender.send_blocking(WorkerMsg::Line(line)); }
                }
            }
            child.wait().map(|s| s.success()).unwrap_or(false)
        }
        Err(e) => { let _ = sender.send_blocking(WorkerMsg::Line(e.to_string())); false }
    }
}

// ── UI ────────────────────────────────────────────────────────────────────────

fn build_ui(app: &Application) {
    let force = std::env::args_os().any(|a| a == "--force" || a == "-f");
    let test_setup = std::env::var("RAKUOS_WELCOME_TEST_SETUP").is_ok();
    if done_file().exists() && !force { return; }

    let json = load_apps_json();
    let mut catalog: Catalog = serde_json::from_str(&json).expect("invalid apps.json");
    for cat in &mut catalog.categories {
        for entry in &mut cat.apps {
            let (path, url) = if let Some(bundled) = rakuos_appstream::bundled_icon_path(&entry.id) {
                (bundled, String::new())
            } else {
                rakuos_appstream::resolve_icon_for_package(&entry.kind, &entry.package, &entry.icon)
            };
            entry.icon_path = path;
            entry.icon_url = url;
        }
    }

    let win = ApplicationWindow::builder()
        .application(app).title("Welcome to RakuOS")
        .default_width(800).default_height(640).build();

    if let Some(display) = gtk4::gdk::Display::default() {
        IconTheme::for_display(&display).add_search_path("/usr/share/pixmaps");
    }
    win.set_icon_name(Some("rakuos-logo"));
    win.set_startup_id(APP_ID);
    let root = GtkBox::new(Orientation::Vertical, 0);
    win.set_content(Some(&root));

    let header_bar = HeaderBar::new();
    if !force {
        header_bar.set_show_start_title_buttons(false);
        header_bar.set_show_end_title_buttons(false);
    }
    root.append(&header_bar);

    if !force {
        win.connect_close_request(|_| gtk4::glib::Propagation::Stop);
    }

    let stack = Stack::new();
    stack.set_transition_type(StackTransitionType::SlideLeftRight);
    stack.set_transition_duration(200);
    stack.set_vexpand(true);
    root.append(&stack);

    root.append(&Separator::new(Orientation::Horizontal));

    let nav = GtkBox::builder()
        .orientation(Orientation::Horizontal).spacing(8)
        .margin_start(24).margin_end(24).margin_top(12).margin_bottom(12).build();

    let dots_box = GtkBox::new(Orientation::Horizontal, 8);
    nav.append(&dots_box);
    let spacer = GtkBox::new(Orientation::Horizontal, 0);
    spacer.set_hexpand(true);
    nav.append(&spacer);

    let back_btn = gtk4::Button::with_label("← Back");
    back_btn.set_visible(false);
    nav.append(&back_btn);

    let next_btn = gtk4::Button::with_label("Next →");
    next_btn.add_css_class("suggested-action");
    nav.append(&next_btn);

    root.append(&nav);

    // Build installed map
    let installed_rpms = query_installed_rpms();
    let installed_flatpaks = query_installed_flatpaks();
    let installed: Arc<HashMap<String, bool>> = Arc::new(
        catalog.categories.iter().flat_map(|c| c.apps.iter())
            .map(|a| (a.id.clone(), is_installed(a, &installed_rpms, &installed_flatpaks)))
            .collect()
    );

    // Page names and indices
    let n_cats = catalog.categories.len();
    let language_page_idx = n_cats + 1;
    let keyboard_page_idx = n_cats + 2;
    let location_page_idx = n_cats + 3;
    let network_page_idx  = n_cats + 4;
    let install_page_idx  = n_cats + 5;
    let done_page_idx     = n_cats + 6;
    let total_pages       = n_cats + 7;

    let mut page_names: Vec<String> = Vec::new();

    build_welcome_page(&stack);
    page_names.push("welcome".to_string());

    // selections: category_id → Vec<(app_id, ToggleButton)>
    let selections: Arc<Mutex<HashMap<String, Vec<(String, ToggleButton)>>>> =
        Arc::new(Mutex::new(HashMap::new()));

    for cat in &catalog.categories {
        let toggles = build_category_page(&stack, cat, installed.as_ref());
        selections.lock().unwrap().insert(cat.id.clone(), toggles);
        page_names.push(cat.id.clone());
    }

    // Setup pages (hidden when --force)
    let lang_widgets = build_language_page(&stack);
    page_names.push("language".to_string());

    let kb_widgets = build_keyboard_page(&stack);
    page_names.push("keyboard".to_string());

    let loc_widgets = build_location_page(&stack);
    page_names.push("location".to_string());

    // Network page
    let net_widgets = build_network_page(&stack);
    page_names.push("network".to_string());

    // Install page
    let install_widgets = build_install_page(&stack);
    page_names.push("install".to_string());

    build_done_page(&stack);
    page_names.push("done".to_string());

    let dot_labels: Vec<Label> = (0..total_pages)
        .map(|_| { let d = Label::new(Some("●")); dots_box.append(&d); d })
        .collect();

    let current = Arc::new(Mutex::new(0usize));
    let next_enabled = Arc::new(AtomicBool::new(true));

    let update_nav = {
        let dot_labels = dot_labels.clone();
        let back_btn = back_btn.clone();
        let next_btn = next_btn.clone();
        let stack = stack.clone();
        let page_names = page_names.clone();
        move |idx: usize, can_next: bool| {
            for (i, dot) in dot_labels.iter().enumerate() {
                if i == idx { dot.add_css_class("accent"); dot.remove_css_class("dim-label"); }
                else { dot.add_css_class("dim-label"); dot.remove_css_class("accent"); }
            }
            back_btn.set_visible(idx > 0);
            next_btn.set_label(if idx == total_pages - 1 { "Finish" } else { "Next →" });
            next_btn.set_sensitive(can_next);
            stack.set_visible_child_name(&page_names[idx]);
        }
    };

    update_nav(0, true);

    let catalog_arc = Arc::new(catalog);

    // ── Next button ──────────────────────────────────────────────────────────

    next_btn.connect_clicked({
        let current        = Arc::clone(&current);
        let win            = win.clone();
        let update_nav     = update_nav.clone();
        let catalog        = Arc::clone(&catalog_arc);
        let selections     = Arc::clone(&selections);
        let installed      = Arc::clone(&installed);
        let lang_widgets   = lang_widgets.clone();
        let kb_widgets     = kb_widgets.clone();
        let loc_widgets    = loc_widgets.clone();
        let net_widgets    = net_widgets.clone();
        let install_widgets = install_widgets.clone();
        let next_btn       = next_btn.clone();
        let back_btn       = back_btn.clone();
        let next_enabled   = Arc::clone(&next_enabled);

        move |_| {
            let mut idx = current.lock().unwrap();
            let n = catalog.categories.len();

            if *idx == total_pages - 1 {
                let df = done_file();
                let _ = std::fs::create_dir_all(df.parent().unwrap());
                let _ = std::fs::File::create(&df);
                win.close();
                return;
            }

            // Last category page → skip setup pages in force mode
            if *idx == n {
                if force && !test_setup {
                    *idx = install_page_idx;
                    drop(idx);
                    install_widgets.page_stack.set_visible_child_name("pre");
                    install_widgets.clear_tasks();
                    update_nav(*current.lock().unwrap(), true);
                } else {
                    *idx = language_page_idx;
                    drop(idx);
                    update_nav(*current.lock().unwrap(), true);
                }
                return;
            }

            if *idx == language_page_idx {
                let locale = lang_widgets.selected.lock().unwrap().clone();
                if !locale.is_empty() {
                    thread::spawn(move || welcome_setup::apply_locale(&locale));
                }
                *idx = keyboard_page_idx;
                drop(idx);
                update_nav(*current.lock().unwrap(), true);
                return;
            }

            if *idx == keyboard_page_idx {
                let layout  = kb_widgets.selected_layout.lock().unwrap().clone();
                let variant = kb_widgets.selected_variant.lock().unwrap().clone();
                if !layout.is_empty() {
                    thread::spawn(move || welcome_setup::apply_keyboard(&layout, &variant));
                }
                *idx = location_page_idx;
                drop(idx);
                update_nav(*current.lock().unwrap(), true);
                return;
            }

            if *idx == location_page_idx {
                let tz = loc_widgets.selected_tz.lock().unwrap().clone();
                if !tz.is_empty() {
                    thread::spawn(move || welcome_setup::apply_timezone(&tz));
                }
                let connected = check_connectivity();
                if connected {
                    *idx = install_page_idx;
                } else {
                    *idx = network_page_idx;
                    net_widgets.start_scan();
                }
                drop(idx);
                install_widgets.page_stack.set_visible_child_name("pre");
                install_widgets.clear_tasks();
                update_nav(*current.lock().unwrap(), true);
                return;
            }

            if *idx == network_page_idx {
                *idx = install_page_idx;
                drop(idx);
                install_widgets.page_stack.set_visible_child_name("pre");
                install_widgets.clear_tasks();
                update_nav(*current.lock().unwrap(), true);
                return;
            }

            *idx += 1;
            let new_idx = *idx;
            drop(idx);
            update_nav(new_idx, true);
        }
    });

    // ── Back button ──────────────────────────────────────────────────────────

    back_btn.connect_clicked({
        let current    = Arc::clone(&current);
        let update_nav = update_nav.clone();
        let next_enabled = Arc::clone(&next_enabled);
        move |_| {
            let mut idx = current.lock().unwrap();
            if *idx > 0 {
                if force && !test_setup && *idx == install_page_idx {
                    // force mode: skip back over all setup+network pages
                    *idx = n_cats;
                } else {
                    *idx -= 1;
                }
                let new_idx = *idx;
                drop(idx);
                update_nav(new_idx, next_enabled.load(Ordering::Relaxed));
            }
        }
    });

    // ── Install button (on the install page's pre-install view) ──────────────
    install_widgets.install_btn.connect_clicked({
        let catalog         = Arc::clone(&catalog_arc);
        let selections      = Arc::clone(&selections);
        let installed       = Arc::clone(&installed);
        let install_widgets = install_widgets.clone();
        let next_btn        = next_btn.clone();
        let back_btn        = back_btn.clone();
        let next_enabled    = Arc::clone(&next_enabled);
        let update_nav      = update_nav.clone();
        let current         = Arc::clone(&current);
        move |_| {
            install_widgets.page_stack.set_visible_child_name("progress");
            begin_install(
                &catalog,
                &selections,
                &installed,
                force,
                &install_widgets,
                &next_btn,
                &back_btn,
                &next_enabled,
                &update_nav,
                &current,
            );
        }
    });

    win.present();
}

// ── Install orchestrator ──────────────────────────────────────────────────────

fn begin_install(
    catalog:         &Arc<Catalog>,
    selections:      &Arc<Mutex<HashMap<String, Vec<(String, ToggleButton)>>>>,
    initial_installed: &Arc<HashMap<String, bool>>,
    force:           bool,
    widgets:         &InstallWidgets,
    next_btn:        &gtk4::Button,
    back_btn:        &gtk4::Button,
    next_enabled:    &Arc<AtomicBool>,
    update_nav:      &(impl Fn(usize, bool) + Clone),
    current:         &Arc<Mutex<usize>>,
) {
    // Build task list
    let mut tasks: Vec<(AppEntry, bool)> = vec![]; // (app, is_remove)
    let sel = selections.lock().unwrap();
    for cat in &catalog.categories {
        if cat.exclusive && !force {
            let cat_toggles = sel.get(&cat.id);
            for app in &cat.apps {
                if *initial_installed.get(&app.id).unwrap_or(&false) && app.kind != "script" {
                    let selected = cat_toggles.map_or(false, |tv| {
                        tv.iter().any(|(id, tb)| id == &app.id && tb.is_active())
                    });
                    if !selected { tasks.push((app.clone(), true)); }
                }
            }
        }
        for app in &cat.apps {
            let selected = sel.get(&cat.id).map_or(false, |tv| {
                tv.iter().any(|(id, tb)| id == &app.id && tb.is_active())
            });
            if selected { tasks.push((app.clone(), false)); }
        }
    }
    drop(sel);

    if tasks.is_empty() {
        widgets.status_label.set_text("No apps selected.");
        return;
    }

    let total_units: usize = tasks.iter().map(|(a, _)| a.unit_weight()).sum();

    // Populate install page with task rows
    widgets.clear_tasks();
    let task_bars: Vec<ProgressBar> = tasks.iter().map(|(app, is_remove)| {
        let row = GtkBox::builder()
            .orientation(Orientation::Horizontal).spacing(8)
            .margin_start(4).margin_end(4).margin_top(4).margin_bottom(4).build();

        let action = if *is_remove { "Remove" } else { "Install" };
        let pkg_count = app.unit_weight();
        let label_text = if pkg_count > 1 {
            format!("{} {} ({} pkgs)", action, app.name, pkg_count)
        } else {
            format!("{} {}", action, app.name)
        };

        let lbl = Label::new(Some(&label_text));
        lbl.set_halign(Align::Start);
        lbl.set_width_chars(32);
        lbl.set_ellipsize(gtk4::pango::EllipsizeMode::End);

        let bar = ProgressBar::new();
        bar.set_hexpand(true);
        bar.set_valign(Align::Center);
        bar.set_fraction(0.0);

        row.append(&lbl);
        row.append(&bar);
        widgets.task_box.append(&row);
        bar
    }).collect();

    let task_bars = Arc::new(task_bars);
    let task_results: Arc<Mutex<Vec<Option<bool>>>> = Arc::new(Mutex::new(vec![None; tasks.len()]));
    let current_task = Arc::new(AtomicI32::new(-1));
    let running = Arc::new(AtomicBool::new(true));

    widgets.overall_bar.set_fraction(0.0);
    widgets.status_label.set_text(&format!("Installing… 0/{} units", total_units));
    next_btn.set_sensitive(false);
    back_btn.set_sensitive(false);
    next_enabled.store(false, Ordering::Relaxed);

    let (sender, receiver) = async_channel::unbounded::<WorkerMsg>();
    let results_for_thread = Arc::clone(&task_results);
    let current_task_thread = Arc::clone(&current_task);
    let running_thread = Arc::clone(&running);

    thread::spawn(move || {
        for (i, (app, is_remove)) in tasks.iter().enumerate() {
            current_task_thread.store(i as i32, Ordering::Relaxed);
            let _ = sender.send_blocking(WorkerMsg::Line(
                if *is_remove { format!("→ Removing {}…", app.name) }
                else          { format!("→ Installing {}…", app.name) }
            ));
            let ok = if *is_remove {
                run_remove(app, &sender)
            } else {
                match app.kind.as_str() {
                    "flatpak" => run_flatpak(app, &sender),
                    "script"  => run_script(&app.package, &sender),
                    _         => run_rpm(app, &sender),
                }
            };
            results_for_thread.lock().unwrap()[i] = Some(ok);
            let _ = sender.send_blocking(WorkerMsg::Line(
                if ok { format!("✓ {} done", app.name) }
                else  { format!("✗ {} failed", app.name) }
            ));
        }
        current_task_thread.store(tasks.len() as i32, Ordering::Relaxed);
        running_thread.store(false, Ordering::Relaxed);
        let _ = sender.send_blocking(WorkerMsg::Done);
    });

    // Poll progress and log on main thread
    let log_buf   = widgets.log_buffer.clone();
    let log_scroll = widgets.log_scroll.clone();
    let overall_bar = widgets.overall_bar.clone();
    let status_lbl  = widgets.status_label.clone();
    let task_bars2  = Arc::clone(&task_bars);
    let results2    = Arc::clone(&task_results);
    let current_task2 = Arc::clone(&current_task);
    let nb = next_btn.clone();
    let bb = back_btn.clone();
    let ne = Arc::clone(&next_enabled);

    glib::MainContext::default().spawn_local(async move {
        let mut units_done = 0usize;
        let mut prev_task_done: Vec<bool> = vec![false; task_bars2.len()];

        while let Ok(msg) = receiver.recv().await {
            match msg {
                WorkerMsg::Line(line) => {
                    log_buf.insert(&mut log_buf.end_iter(), &format!("{line}\n"));
                    log_scroll.vadjustment().set_value(log_scroll.vadjustment().upper());

                    // Update per-bar progress
                    let cur = current_task2.load(Ordering::Relaxed) as usize;
                    if cur < task_bars2.len() {
                        task_bars2[cur].pulse();
                    }
                    // Mark completed bars
                    let results = results2.lock().unwrap();
                    for (i, bar) in task_bars2.iter().enumerate() {
                        if !prev_task_done[i] {
                            if let Some(Some(_ok)) = results.get(i) {
                                bar.set_fraction(1.0);
                                prev_task_done[i] = true;
                                units_done += 1; // approximate; fine for display
                            }
                        }
                    }
                    drop(results);
                    let frac = (units_done as f64) / (task_bars2.len() as f64).max(1.0);
                    overall_bar.set_fraction(frac.min(1.0));
                    status_lbl.set_text(&format!("Installing… {}/{} tasks", units_done, task_bars2.len()));
                }
                WorkerMsg::Done => {
                    overall_bar.set_fraction(1.0);
                    status_lbl.set_text(&format!("Done — {}/{} tasks completed", task_bars2.len(), task_bars2.len()));
                    // Mark any remaining bars complete
                    let results = results2.lock().unwrap();
                    for (i, bar) in task_bars2.iter().enumerate() {
                        if let Some(Some(ok)) = results.get(i) {
                            bar.set_fraction(1.0);
                            if !ok { bar.add_css_class("error"); }
                        }
                    }
                    drop(results);
                    nb.set_sensitive(true);
                    bb.set_sensitive(true);
                    ne.store(true, Ordering::Relaxed);
                    break;
                }
            }
        }
    });
}

// ── Network page widgets ──────────────────────────────────────────────────────

#[derive(Clone)]
struct NetworkWidgets {
    net_list:      ListBox,
    password_entry: Entry,
    connect_btn:   gtk4::Button,
    status_label:  Label,
    selected_ssid: Arc<Mutex<String>>,
    scan_results:  Arc<Mutex<Option<Vec<WifiNetwork>>>>,
}

impl NetworkWidgets {
    fn start_scan(&self) {
        let results = Arc::clone(&self.scan_results);
        let net_list = self.net_list.clone();
        let selected = Arc::clone(&self.selected_ssid);

        // Clear list
        while let Some(child) = net_list.first_child() { net_list.remove(&child); }

        let spinner_row = Label::new(Some("⟳ Scanning for networks…"));
        net_list.append(&spinner_row);

        let (tx, rx) = std::sync::mpsc::channel::<Vec<WifiNetwork>>();
        thread::spawn(move || { let _ = tx.send(scan_wifi()); });

        glib::timeout_add_local(Duration::from_millis(200), move || {
            match rx.try_recv() {
                Ok(nets) => {
                    while let Some(child) = net_list.first_child() { net_list.remove(&child); }
                    *results.lock().unwrap() = Some(nets.clone());
                    if nets.is_empty() {
                        net_list.append(&Label::new(Some("No networks found.")));
                    }
                    for net in &nets {
                        let bars = match net.signal {
                            80..=100 => "▪▪▪▪",
                            60..=79  => "▪▪▪░",
                            40..=59  => "▪▪░░",
                            _        => "▪░░░",
                        };
                        let lock = if net.secured { " 🔒" } else { "" };
                        let row_box = GtkBox::new(Orientation::Horizontal, 8);
                        let lbl = Label::new(Some(&format!("{}{} {}", bars, lock, net.ssid)));
                        lbl.set_halign(Align::Start);
                        lbl.set_hexpand(true);
                        let sig = Label::new(Some(&format!("{}%", net.signal)));
                        row_box.append(&lbl);
                        row_box.append(&sig);
                        row_box.set_margin_start(8);
                        row_box.set_margin_end(8);
                        row_box.set_margin_top(4);
                        row_box.set_margin_bottom(4);
                        net_list.append(&row_box);
                    }
                    // Wire row-selected signal
                    let nets_for_signal = nets.clone();
                    let selected2 = Arc::clone(&selected);
                    net_list.connect_row_selected(move |_, row| {
                        if let Some(r) = row {
                            let idx = r.index() as usize;
                            if let Some(net) = nets_for_signal.get(idx) {
                                *selected2.lock().unwrap() = net.ssid.clone();
                            }
                        }
                    });
                    glib::ControlFlow::Break
                }
                Err(std::sync::mpsc::TryRecvError::Empty) => glib::ControlFlow::Continue,
                Err(_) => glib::ControlFlow::Break,
            }
        });
    }
}

// ── Install page widgets ──────────────────────────────────────────────────────

#[derive(Clone)]
struct InstallWidgets {
    overall_bar:  ProgressBar,
    status_label: Label,
    task_box:     GtkBox,
    log_buffer:   gtk4::TextBuffer,
    log_scroll:   ScrolledWindow,
    page_stack:   Stack,
    install_btn:  gtk4::Button,
}

impl InstallWidgets {
    fn clear_tasks(&self) {
        while let Some(child) = self.task_box.first_child() {
            self.task_box.remove(&child);
        }
        self.log_buffer.set_text("");
    }
}

// ── Page builders ─────────────────────────────────────────────────────────────

fn build_welcome_page(stack: &Stack) {
    let style = StyleManager::default();
    let logo_path = if style.is_dark() { LOGO_DARK } else { LOGO_LIGHT };

    let page = GtkBox::builder()
        .orientation(Orientation::Vertical).spacing(16)
        .valign(Align::Center).halign(Align::Center)
        .margin_start(60).margin_end(60).margin_top(40).margin_bottom(40).build();

    let img = gtk4::Image::from_file(logo_path);
    img.set_pixel_size(300);
    img.set_halign(Align::Center);
    page.append(&img);

    let heading = Label::new(Some("Welcome to RakuOS Linux"));
    heading.add_css_class("title-1");
    heading.set_halign(Align::Center);
    page.append(&heading);

    let tagline = Label::new(Some("The Hybrid Atomic Linux Desktop"));
    tagline.add_css_class("title-4");
    tagline.set_halign(Align::Center);
    page.append(&tagline);

    let desc = Label::new(Some(
        "RakuOS combines the stability and security of an atomic immutable base \
         with the flexibility of a traditional Linux distribution. \
         Your system can never be broken by a bad update.",
    ));
    desc.set_wrap(true);
    desc.set_max_width_chars(70);
    desc.set_halign(Align::Center);
    desc.set_justify(gtk4::Justification::Center);
    page.append(&desc);

    page.append(&Separator::new(Orientation::Horizontal));
    let links_label = Label::new(Some("Find us online:"));
    links_label.set_halign(Align::Center);
    page.append(&links_label);

    let links_box = GtkBox::builder().orientation(Orientation::Horizontal).spacing(24).halign(Align::Center).build();
    for (text, url) in [
        ("🌐 Website", "https://rakuos.org"),
        ("💻 GitLab", "https://gitlab.com/rakuos"),
        ("📦 SourceForge", "https://sourceforge.net/projects/rakuos/"),
    ] {
        links_box.append(&gtk4::LinkButton::with_label(url, text));
    }
    page.append(&links_box);
    stack.add_named(&page, Some("welcome"));
}

fn build_category_page(stack: &Stack, cat: &Category, installed: &HashMap<String, bool>) -> Vec<(String, ToggleButton)> {
    let scroll = ScrolledWindow::builder()
        .hscrollbar_policy(PolicyType::Never).vexpand(true).build();

    let outer = GtkBox::builder()
        .orientation(Orientation::Vertical).spacing(16)
        .margin_start(24).margin_end(24).margin_top(20).margin_bottom(16).build();

    let heading = Label::new(Some(&cat.title));
    heading.add_css_class("title-2");
    heading.set_halign(Align::Start);
    outer.append(&heading);

    let desc = Label::new(Some(&cat.description));
    desc.set_wrap(true);
    desc.set_halign(Align::Start);
    outer.append(&desc);

    if cat.exclusive {
        let hint = Label::new(Some("Select one option."));
        hint.add_css_class("caption");
        hint.set_halign(Align::Start);
        outer.append(&hint);
    }

    let flow = FlowBox::builder()
        .selection_mode(SelectionMode::None).homogeneous(true)
        .column_spacing(10).row_spacing(10)
        .min_children_per_line(2).max_children_per_line(5).build();
    outer.append(&flow);

    let mut toggles = Vec::new();

    for entry in &cat.apps {
        let icon_widget = make_icon_widget(&entry.icon_path, &entry.icon_url, 56, &entry.name);

        let name_lbl = Label::new(Some(&entry.name));
        name_lbl.add_css_class("heading");
        name_lbl.set_halign(Align::Center);
        name_lbl.set_ellipsize(gtk4::pango::EllipsizeMode::End);
        name_lbl.set_max_width_chars(16);

        let summary_lbl = Label::new(Some(&entry.summary));
        summary_lbl.add_css_class("caption");
        summary_lbl.set_halign(Align::Center);
        summary_lbl.set_justify(gtk4::Justification::Center);
        summary_lbl.set_wrap(true);
        summary_lbl.set_lines(2);
        summary_lbl.set_ellipsize(gtk4::pango::EllipsizeMode::End);
        summary_lbl.set_max_width_chars(20);

        let inner = GtkBox::builder()
            .orientation(Orientation::Vertical).spacing(8)
            .margin_start(12).margin_end(12).margin_top(16).margin_bottom(12).build();
        inner.append(&icon_widget);
        inner.append(&name_lbl);
        inner.append(&summary_lbl);

        if entry.kind == "flatpak" {
            let badge = Label::new(Some("Flatpak"));
            badge.add_css_class("caption");
            badge.add_css_class("accent");
            badge.set_halign(Align::Center);
            inner.append(&badge);
        }

        let toggle = ToggleButton::new();
        toggle.set_active(*installed.get(&entry.id).unwrap_or(&false));
        toggle.set_child(Some(&inner));
        toggle.add_css_class("card");
        toggle.set_size_request(150, 175);

        flow.append(&toggle);
        toggles.push((entry.id.clone(), toggle));
    }

    // Exclusive: wire mutual deselection
    if cat.exclusive {
        let all: Vec<ToggleButton> = toggles.iter().map(|(_, t)| t.clone()).collect();
        for (i, (_, btn)) in toggles.iter().enumerate() {
            let others: Vec<ToggleButton> = all.iter().enumerate()
                .filter(|(j, _)| *j != i)
                .map(|(_, t)| t.clone())
                .collect();
            btn.connect_toggled(move |b| {
                if b.is_active() {
                    for other in &others {
                        if other.is_active() { other.set_active(false); }
                    }
                }
            });
        }
    }

    scroll.set_child(Some(&outer));
    stack.add_named(&scroll, Some(&cat.id));
    toggles
}

// ── Language page ─────────────────────────────────────────────────────────────

#[derive(Clone)]
struct LanguageWidgets {
    selected: Arc<Mutex<String>>,
}

fn build_language_page(stack: &Stack) -> LanguageWidgets {
    let selected: Arc<Mutex<String>> = Arc::new(Mutex::new(String::new()));

    let outer = GtkBox::builder()
        .orientation(Orientation::Vertical).spacing(0).build();

    // Header
    let header = GtkBox::builder()
        .orientation(Orientation::Vertical).spacing(4)
        .margin_start(24).margin_end(24).margin_top(20).margin_bottom(16).build();
    let heading = Label::new(Some("Language"));
    heading.add_css_class("title-2");
    heading.set_halign(Align::Start);
    let subtitle = Label::new(Some("Choose the language for your system."));
    subtitle.add_css_class("dim-label");
    subtitle.set_halign(Align::Start);
    header.append(&heading);
    header.append(&subtitle);
    outer.append(&header);
    outer.append(&Separator::new(Orientation::Horizontal));

    let content = GtkBox::builder()
        .orientation(Orientation::Vertical).spacing(8)
        .margin_start(24).margin_end(24).margin_top(16).margin_bottom(16)
        .vexpand(true).build();

    let search = gtk4::SearchEntry::new();
    search.set_placeholder_text(Some("Search languages…"));
    content.append(&search);

    // Selected indicator
    let sel_box = GtkBox::builder()
        .orientation(Orientation::Horizontal).spacing(6)
        .margin_start(10).margin_end(10).margin_top(6).margin_bottom(6).build();
    let sel_check = Label::new(Some("✓"));
    sel_check.add_css_class("accent");
    let sel_label = Label::new(None);
    sel_label.add_css_class("accent");
    sel_label.set_hexpand(true);
    sel_label.set_halign(Align::Start);
    sel_label.set_ellipsize(gtk4::pango::EllipsizeMode::End);
    sel_box.append(&sel_check);
    sel_box.append(&sel_label);
    let sel_frame = gtk4::Frame::new(None);
    sel_frame.add_css_class("card");
    sel_frame.set_child(Some(&sel_box));
    sel_frame.set_visible(false);
    content.append(&sel_frame);

    let list = ListBox::builder()
        .selection_mode(SelectionMode::Single)
        .css_classes(vec!["boxed-list"])
        .build();
    let scroll = ScrolledWindow::builder()
        .child(&list).vexpand(true)
        .hscrollbar_policy(PolicyType::Never).build();
    content.append(&scroll);
    outer.append(&content);

    // Populate locales — two columns: display name + code
    let locales = welcome_setup::list_locales();
    for entry in &locales {
        let row_box = GtkBox::builder()
            .orientation(Orientation::Horizontal).spacing(8)
            .margin_start(12).margin_end(12).margin_top(8).margin_bottom(8).build();
        let name_lbl = Label::new(Some(&entry.display_name));
        name_lbl.set_halign(Align::Start);
        name_lbl.set_hexpand(true);
        name_lbl.set_ellipsize(gtk4::pango::EllipsizeMode::End);
        let code_lbl = Label::new(Some(&entry.code));
        code_lbl.add_css_class("dim-label");
        code_lbl.add_css_class("caption");
        code_lbl.set_halign(Align::End);
        row_box.append(&name_lbl);
        row_box.append(&code_lbl);
        list.append(&row_box);
    }

    // Filter on search
    let locales_rc = std::rc::Rc::new(locales);
    {
        let list = list.clone();
        let locales = locales_rc.clone();
        search.connect_search_changed(move |entry| {
            let query = entry.text().to_lowercase();
            let mut row = list.first_child();
            let mut i = 0usize;
            while let Some(r) = row {
                let visible = query.is_empty()
                    || locales.get(i).is_some_and(|l| {
                        l.display_name.to_lowercase().contains(&query)
                            || l.code.to_lowercase().contains(&query)
                    });
                r.set_visible(visible);
                row = r.next_sibling();
                i += 1;
            }
        });
    }

    // Track selection
    {
        let selected = Arc::clone(&selected);
        let locales = locales_rc.clone();
        let sel_frame = sel_frame.clone();
        let sel_label = sel_label.clone();
        list.connect_row_selected(move |_, row| {
            if let Some(r) = row {
                let idx = r.index() as usize;
                if let Some(entry) = locales.get(idx) {
                    *selected.lock().unwrap() = entry.code.clone();
                    sel_label.set_text(&entry.display_name);
                    sel_frame.set_visible(true);
                }
            }
        });
    }

    stack.add_named(&outer, Some("language"));
    LanguageWidgets { selected }
}

// ── Keyboard page ─────────────────────────────────────────────────────────────

#[derive(Clone)]
struct KeyboardWidgets {
    selected_layout:  Arc<Mutex<String>>,
    selected_variant: Arc<Mutex<String>>,
}

fn build_keyboard_page(stack: &Stack) -> KeyboardWidgets {
    let selected_layout:  Arc<Mutex<String>> = Arc::new(Mutex::new(String::new()));
    let selected_variant: Arc<Mutex<String>> = Arc::new(Mutex::new(String::new()));

    let outer = GtkBox::builder()
        .orientation(Orientation::Vertical).spacing(0).build();

    // Header
    let header = GtkBox::builder()
        .orientation(Orientation::Vertical).spacing(4)
        .margin_start(24).margin_end(24).margin_top(20).margin_bottom(16).build();
    let heading = Label::new(Some("Keyboard Layout"));
    heading.add_css_class("title-2");
    heading.set_halign(Align::Start);
    let subtitle = Label::new(Some("Choose the keyboard layout for your system."));
    subtitle.add_css_class("dim-label");
    subtitle.set_halign(Align::Start);
    header.append(&heading);
    header.append(&subtitle);
    outer.append(&header);
    outer.append(&Separator::new(Orientation::Horizontal));

    let content = GtkBox::builder()
        .orientation(Orientation::Vertical).spacing(8)
        .margin_start(24).margin_end(24).margin_top(16).margin_bottom(16)
        .vexpand(true).build();

    let search = gtk4::SearchEntry::new();
    search.set_placeholder_text(Some("Search keyboard layouts…"));
    content.append(&search);

    // Selected indicator
    let sel_box = GtkBox::builder()
        .orientation(Orientation::Horizontal).spacing(6)
        .margin_start(10).margin_end(10).margin_top(6).margin_bottom(6).build();
    let sel_check = Label::new(Some("✓"));
    sel_check.add_css_class("accent");
    let sel_label = Label::new(None);
    sel_label.add_css_class("accent");
    sel_label.set_hexpand(true);
    sel_label.set_halign(Align::Start);
    sel_label.set_ellipsize(gtk4::pango::EllipsizeMode::End);
    sel_box.append(&sel_check);
    sel_box.append(&sel_label);
    let sel_frame = gtk4::Frame::new(None);
    sel_frame.add_css_class("card");
    sel_frame.set_child(Some(&sel_box));
    sel_frame.set_visible(false);
    content.append(&sel_frame);

    let list = ListBox::builder()
        .selection_mode(SelectionMode::Single)
        .css_classes(vec!["boxed-list"])
        .build();
    let scroll = ScrolledWindow::builder()
        .child(&list).vexpand(true)
        .hscrollbar_policy(PolicyType::Never).build();
    content.append(&scroll);
    outer.append(&content);

    // Two-column rows: description + layout[/variant] code
    let entries = welcome_setup::keyboard_layout_entries();
    for entry in &entries {
        let row_box = GtkBox::builder()
            .orientation(Orientation::Horizontal).spacing(8)
            .margin_start(12).margin_end(12).margin_top(8).margin_bottom(8).build();
        let desc_lbl = Label::new(Some(&entry.description));
        desc_lbl.set_halign(Align::Start);
        desc_lbl.set_hexpand(true);
        desc_lbl.set_ellipsize(gtk4::pango::EllipsizeMode::End);
        let code_text = if entry.variant.is_empty() {
            entry.layout.clone()
        } else {
            format!("{}/{}", entry.layout, entry.variant)
        };
        let code_lbl = Label::new(Some(&code_text));
        code_lbl.add_css_class("dim-label");
        code_lbl.add_css_class("caption");
        code_lbl.set_halign(Align::End);
        row_box.append(&desc_lbl);
        row_box.append(&code_lbl);
        list.append(&row_box);
    }

    let entries_rc = std::rc::Rc::new(entries);
    {
        let list = list.clone();
        let entries = entries_rc.clone();
        search.connect_search_changed(move |entry| {
            let query = entry.text().to_lowercase();
            let mut row = list.first_child();
            let mut i = 0usize;
            while let Some(r) = row {
                let visible = query.is_empty()
                    || entries.get(i).is_some_and(|e| e.description.to_lowercase().contains(&query));
                r.set_visible(visible);
                row = r.next_sibling();
                i += 1;
            }
        });
    }

    {
        let selected_layout  = Arc::clone(&selected_layout);
        let selected_variant = Arc::clone(&selected_variant);
        let entries = entries_rc.clone();
        let sel_frame = sel_frame.clone();
        let sel_label = sel_label.clone();
        list.connect_row_selected(move |_, row| {
            if let Some(r) = row {
                let idx = r.index() as usize;
                if let Some(e) = entries.get(idx) {
                    *selected_layout.lock().unwrap()  = e.layout.clone();
                    *selected_variant.lock().unwrap() = e.variant.clone();
                    sel_label.set_text(&e.description);
                    sel_frame.set_visible(true);
                }
            }
        });
    }

    stack.add_named(&outer, Some("keyboard"));
    KeyboardWidgets { selected_layout, selected_variant }
}

// ── Location page ─────────────────────────────────────────────────────────────

#[derive(Clone)]
struct LocationWidgets {
    selected_tz: Arc<Mutex<String>>,
}

fn build_location_page(stack: &Stack) -> LocationWidgets {
    let selected_tz: Arc<Mutex<String>> = Arc::new(Mutex::new(String::new()));
    let cities = std::rc::Rc::new(welcome_setup::load_cities());

    let outer = GtkBox::builder()
        .orientation(Orientation::Vertical).spacing(0).build();

    // Header
    let header = GtkBox::builder()
        .orientation(Orientation::Vertical).spacing(4)
        .margin_start(24).margin_end(24).margin_top(20).margin_bottom(16).build();
    let heading = Label::new(Some("Timezone"));
    heading.add_css_class("title-2");
    heading.set_halign(Align::Start);
    let subtitle = Label::new(Some("Search for your nearest major city to set the timezone."));
    subtitle.add_css_class("dim-label");
    subtitle.set_halign(Align::Start);
    header.append(&heading);
    header.append(&subtitle);
    outer.append(&header);
    outer.append(&Separator::new(Orientation::Horizontal));

    let content = GtkBox::builder()
        .orientation(Orientation::Vertical).spacing(8)
        .margin_start(24).margin_end(24).margin_top(16).margin_bottom(16)
        .vexpand(true).build();

    let search = gtk4::SearchEntry::new();
    search.set_placeholder_text(Some("Search for a city…"));
    content.append(&search);

    // Selected indicator
    let sel_box = GtkBox::builder()
        .orientation(Orientation::Horizontal).spacing(6)
        .margin_start(10).margin_end(10).margin_top(6).margin_bottom(6).build();
    let sel_check = Label::new(Some("✓"));
    sel_check.add_css_class("accent");
    let sel_label = Label::new(None);
    sel_label.add_css_class("accent");
    sel_label.set_hexpand(true);
    sel_label.set_halign(Align::Start);
    sel_label.set_ellipsize(gtk4::pango::EllipsizeMode::End);
    sel_box.append(&sel_check);
    sel_box.append(&sel_label);
    let sel_frame = gtk4::Frame::new(None);
    sel_frame.add_css_class("card");
    sel_frame.set_child(Some(&sel_box));
    sel_frame.set_visible(false);
    content.append(&sel_frame);

    // Empty state shown before any search
    let empty_state = GtkBox::builder()
        .orientation(Orientation::Vertical).spacing(8)
        .valign(Align::Center).halign(Align::Center)
        .vexpand(true).build();
    let globe_lbl = Label::new(None);
    globe_lbl.set_markup("<span size=\"4000%\">🌐</span>");
    globe_lbl.set_halign(Align::Center);
    let empty_hint = Label::new(Some("Search for a city above"));
    empty_hint.add_css_class("dim-label");
    empty_hint.set_halign(Align::Center);
    empty_state.append(&globe_lbl);
    empty_state.append(&empty_hint);
    content.append(&empty_state);

    let list = ListBox::builder()
        .selection_mode(SelectionMode::Single)
        .css_classes(vec!["boxed-list"])
        .build();
    let scroll = ScrolledWindow::builder()
        .child(&list).vexpand(true)
        .hscrollbar_policy(PolicyType::Never).build();
    scroll.set_visible(false);
    content.append(&scroll);
    outer.append(&content);

    let shown: std::rc::Rc<std::cell::RefCell<Vec<usize>>> =
        std::rc::Rc::new(std::cell::RefCell::new(vec![]));

    {
        let list = list.clone();
        let cities = cities.clone();
        let shown = shown.clone();
        let scroll = scroll.clone();
        let empty_state = empty_state.clone();
        search.connect_search_changed(move |entry| {
            let query = entry.text().to_lowercase();
            while let Some(child) = list.first_child() { list.remove(&child); }
            let mut new_shown = vec![];

            if query.is_empty() {
                scroll.set_visible(false);
                empty_state.set_visible(true);
            } else {
                scroll.set_visible(true);
                empty_state.set_visible(false);
                for (i, city) in cities.iter().enumerate() {
                    if city.name.to_lowercase().contains(&query)
                        || city.alternate_names.iter().any(|n| n.to_lowercase().contains(&query))
                    {
                        // Two-line row: city name + timezone
                        let row_box = GtkBox::builder()
                            .orientation(Orientation::Vertical).spacing(2)
                            .margin_start(12).margin_end(12).margin_top(8).margin_bottom(8).build();
                        let city_lbl = Label::new(Some(&format!("{}", city.name)));
                        city_lbl.set_halign(Align::Start);
                        city_lbl.set_hexpand(true);
                        let tz_lbl = Label::new(Some(&format!("{}", city.timezone)));
                        tz_lbl.add_css_class("dim-label");
                        tz_lbl.add_css_class("caption");
                        tz_lbl.set_halign(Align::Start);
                        row_box.append(&city_lbl);
                        row_box.append(&tz_lbl);
                        list.append(&row_box);
                        new_shown.push(i);
                        if new_shown.len() >= 100 { break; }
                    }
                }
            }
            *shown.borrow_mut() = new_shown;
        });
    }

    {
        let selected_tz = Arc::clone(&selected_tz);
        let cities = cities.clone();
        let shown = shown.clone();
        let sel_frame = sel_frame.clone();
        let sel_label = sel_label.clone();
        list.connect_row_selected(move |_, row| {
            if let Some(r) = row {
                let row_idx = r.index() as usize;
                if let Some(&city_idx) = shown.borrow().get(row_idx) {
                    if let Some(city) = cities.get(city_idx) {
                        *selected_tz.lock().unwrap() = city.timezone.to_string();
                        sel_label.set_text(&format!("{} — {}", city.name, city.timezone));
                        sel_frame.set_visible(true);
                    }
                }
            }
        });
    }

    stack.add_named(&outer, Some("location"));
    LocationWidgets { selected_tz }
}

fn build_network_page(stack: &Stack) -> NetworkWidgets {
    let scroll = ScrolledWindow::builder()
        .hscrollbar_policy(PolicyType::Never).vexpand(true).build();

    let outer = GtkBox::builder()
        .orientation(Orientation::Vertical).spacing(12)
        .margin_start(24).margin_end(24).margin_top(20).margin_bottom(16).build();

    let heading = Label::new(Some("Network Setup"));
    heading.add_css_class("title-2");
    heading.set_halign(Align::Start);
    outer.append(&heading);

    let desc = Label::new(Some(
        "No internet connection detected. Connect to WiFi to install apps, \
         or click Next to skip and install later."
    ));
    desc.set_wrap(true);
    desc.set_halign(Align::Start);
    outer.append(&desc);

    outer.append(&Separator::new(Orientation::Horizontal));

    // Scan header
    let scan_row = GtkBox::builder().orientation(Orientation::Horizontal).spacing(8).build();
    let net_heading = Label::new(Some("Available Networks:"));
    net_heading.set_hexpand(true);
    net_heading.set_halign(Align::Start);
    let scan_btn = gtk4::Button::with_label("↺ Scan");
    scan_row.append(&net_heading);
    scan_row.append(&scan_btn);
    outer.append(&scan_row);

    // Network list
    let net_list = ListBox::builder()
        .selection_mode(SelectionMode::Single)
        .css_classes(vec!["boxed-list"])
        .build();
    let net_scroll = ScrolledWindow::builder()
        .child(&net_list).min_content_height(160).max_content_height(160)
        .hscrollbar_policy(PolicyType::Never).build();
    outer.append(&net_scroll);

    // Password section
    let pw_label = Label::new(Some("WiFi Password:"));
    pw_label.set_halign(Align::Start);
    outer.append(&pw_label);

    let password_entry = Entry::builder().placeholder_text("Enter password").visibility(false).build();
    outer.append(&password_entry);

    let connect_btn = gtk4::Button::with_label("Connect");
    connect_btn.add_css_class("suggested-action");
    outer.append(&connect_btn);

    let status_label = Label::new(None);
    status_label.set_halign(Align::Start);
    status_label.set_wrap(true);
    outer.append(&status_label);

    scroll.set_child(Some(&outer));
    stack.add_named(&scroll, Some("network"));

    let selected_ssid: Arc<Mutex<String>> = Arc::new(Mutex::new(String::new()));
    let scan_results: Arc<Mutex<Option<Vec<WifiNetwork>>>> = Arc::new(Mutex::new(None));

    let widgets = NetworkWidgets {
        net_list: net_list.clone(),
        password_entry: password_entry.clone(),
        connect_btn: connect_btn.clone(),
        status_label: status_label.clone(),
        selected_ssid: Arc::clone(&selected_ssid),
        scan_results: Arc::clone(&scan_results),
    };

    // Scan button
    let w2 = widgets.clone();
    scan_btn.connect_clicked(move |_| { w2.start_scan(); });

    // Connect button
    let sel = Arc::clone(&selected_ssid);
    let pw_entry = password_entry.clone();
    let status_lbl = status_label.clone();
    let cb = connect_btn.clone();
    connect_btn.connect_clicked(move |_| {
        let ssid = sel.lock().unwrap().clone();
        if ssid.is_empty() {
            status_lbl.set_text("Please select a network first.");
            return;
        }
        let pw = pw_entry.text().to_string();
        cb.set_sensitive(false);
        status_lbl.set_text("⟳ Connecting…");
        let ssid2 = ssid.clone();
        let (tx, rx) = std::sync::mpsc::channel::<bool>();
        thread::spawn(move || { let _ = tx.send(connect_wifi(&ssid2, &pw)); });
        let sl = status_lbl.clone();
        let cb2 = cb.clone();
        glib::timeout_add_local(Duration::from_millis(200), move || {
            match rx.try_recv() {
                Ok(ok) => {
                    cb2.set_sensitive(true);
                    sl.set_text(if ok { "✓ Connected! Click Next to continue." } else { "✗ Failed — check password." });
                    glib::ControlFlow::Break
                }
                Err(std::sync::mpsc::TryRecvError::Empty) => glib::ControlFlow::Continue,
                Err(_) => { cb2.set_sensitive(true); glib::ControlFlow::Break }
            }
        });
    });

    widgets
}

fn build_install_page(stack: &Stack) -> InstallWidgets {
    let page_stack = Stack::new();
    page_stack.set_vexpand(true);

    // ── Pre-install: centered call-to-action ──────────────────────────────────
    let pre = GtkBox::builder()
        .orientation(Orientation::Vertical).spacing(20)
        .valign(Align::Center).halign(Align::Center)
        .margin_start(60).margin_end(60).build();

    let pre_heading = Label::new(Some("Ready to Install"));
    pre_heading.add_css_class("title-1");
    pre_heading.set_halign(Align::Center);
    pre.append(&pre_heading);

    let pre_desc = Label::new(Some(
        "Your selected apps will now be installed.\nThis may take a few minutes.",
    ));
    pre_desc.set_halign(Align::Center);
    pre_desc.set_justify(gtk4::Justification::Center);
    pre_desc.set_wrap(true);
    pre.append(&pre_desc);

    let install_btn = gtk4::Button::with_label("Install Apps");
    install_btn.add_css_class("suggested-action");
    install_btn.add_css_class("pill");
    install_btn.set_halign(Align::Center);
    install_btn.set_size_request(160, 44);
    pre.append(&install_btn);

    page_stack.add_named(&pre, Some("pre"));

    // ── Progress view ─────────────────────────────────────────────────────────
    let outer = GtkBox::builder()
        .orientation(Orientation::Vertical).spacing(8)
        .margin_start(24).margin_end(24).margin_top(20).margin_bottom(16).build();

    let heading = Label::new(Some("Installing Your Apps"));
    heading.add_css_class("title-2");
    heading.set_halign(Align::Start);
    outer.append(&heading);

    let status_label = Label::new(Some("Starting…"));
    status_label.set_halign(Align::Start);
    outer.append(&status_label);

    let overall_bar = ProgressBar::new();
    overall_bar.set_fraction(0.0);
    outer.append(&overall_bar);

    outer.append(&Separator::new(Orientation::Horizontal));

    let task_box = GtkBox::builder()
        .orientation(Orientation::Vertical).spacing(4).build();
    let task_scroll = ScrolledWindow::builder()
        .child(&task_box).vexpand(true).min_content_height(200)
        .hscrollbar_policy(PolicyType::Never).build();
    outer.append(&task_scroll);

    outer.append(&Separator::new(Orientation::Horizontal));

    let log_view = TextView::new();
    log_view.set_editable(false);
    log_view.set_monospace(true);
    log_view.set_wrap_mode(WrapMode::WordChar);
    let log_buffer = log_view.buffer();
    let log_scroll = ScrolledWindow::builder()
        .child(&log_view).min_content_height(100).max_content_height(100)
        .hscrollbar_policy(PolicyType::Never).build();
    outer.append(&log_scroll);

    let progress_scroll = ScrolledWindow::builder()
        .child(&outer).hscrollbar_policy(PolicyType::Never).vexpand(true).build();
    page_stack.add_named(&progress_scroll, Some("progress"));

    stack.add_named(&page_stack, Some("install"));

    InstallWidgets { overall_bar, status_label, task_box, log_buffer, log_scroll, page_stack, install_btn }
}

fn build_done_page(stack: &Stack) {
    let page = GtkBox::builder()
        .orientation(Orientation::Vertical).spacing(16)
        .valign(Align::Center).halign(Align::Center)
        .margin_start(60).margin_end(60).margin_top(40).margin_bottom(40).build();

    let icon = Label::new(None);
    icon.set_markup("<span size=\"3000%\">🎉</span>");
    icon.set_halign(Align::Center);
    page.append(&icon);

    let heading = Label::new(Some("You're All Set!"));
    heading.add_css_class("title-1");
    heading.set_halign(Align::Center);
    page.append(&heading);

    let desc = Label::new(Some(
        "RakuOS is ready to use. You can install more software from the RakuOS Software Center.",
    ));
    desc.set_wrap(true);
    desc.set_max_width_chars(70);
    desc.set_halign(Align::Center);
    desc.set_justify(gtk4::Justification::Center);
    page.append(&desc);

    stack.add_named(&page, Some("done"));
}

fn main() {
    let args: Vec<String> = std::env::args()
        .filter(|a| a != "--force" && a != "-f")
        .collect();
    let app = Application::builder().application_id(APP_ID).build();
    app.connect_activate(build_ui);
    app.run_with_args(&args);
}
