use super::CommandRunner;
use crate::arch::engine::{InstallContext, QuestionId};
use crate::arch::mkinitcpio::MkinitcpioConfig;
use anyhow::{Context, Result};
use std::process::Command;

/// Groups that users should be added to for normal desktop usage
pub const USER_GROUPS: &[&str] = &["wheel", "video", "docker", "sys", "rfkill"];

/// System groups that need to exist but users shouldn't be members of
pub const SYSTEM_GROUPS: &[&str] = &["nobody"];

/// Ensure required groups exist on the system.
///
/// This creates both user groups (that users should be members of) and
/// system groups (that need to exist but users shouldn't be members of).
///
/// This is called by both `configure_users` during fresh install and
/// `setup_instantos` during `ins arch setup`.
pub fn ensure_groups_exist(executor: &dyn CommandRunner) -> Result<()> {
    // Ensure user groups exist
    for group in USER_GROUPS {
        let mut cmd = Command::new("groupadd");
        cmd.arg("-f").arg(group);
        executor.run(&mut cmd)?;
    }

    // Ensure system groups exist (these are not for user membership)
    for group in SYSTEM_GROUPS {
        let mut cmd = Command::new("groupadd");
        cmd.arg("-f").arg(group);
        executor.run(&mut cmd)?;
    }

    Ok(())
}

/// Add an existing user to the standard user groups.
///
/// This is used by `ins arch setup` to add an existing user to the required groups.
pub fn add_user_to_groups(username: &str, executor: &dyn CommandRunner) -> Result<()> {
    println!(
        "Adding user {} to groups: {}",
        username,
        USER_GROUPS.join(", ")
    );
    let mut cmd = Command::new("usermod");
    cmd.arg("-aG").arg(USER_GROUPS.join(",")).arg(username);
    executor.run(&mut cmd)?;
    Ok(())
}

pub async fn install_config(context: &InstallContext, executor: &dyn CommandRunner) -> Result<()> {
    println!("Configuring system (inside chroot)...");

    // Enable multilib for 32-bit support (needed for lib32-vulkan-* GPU drivers)
    // This runs before package installation so lib32 packages can be installed.
    println!("Enabling multilib repository...");
    crate::common::pacman::enable_multilib(executor.dry_run()).await?;

    // Update repos after enabling multilib
    sync_repos(executor)?;

    configure_pacman_target(executor).await?;
    install_standard_packages(context, executor)?;
    configure_timezone(context, executor)?;
    configure_locale(context, executor)?;
    configure_network(context, executor)?;
    configure_users(context, executor)?;
    configure_environment(executor)?;
    configure_vconsole(context, executor)?;
    configure_sudo(context, executor)?;
    configure_mkinitcpio(context, executor)?;

    Ok(())
}

/// Configure global environment variables
pub fn configure_environment(executor: &dyn CommandRunner) -> Result<()> {
    println!("Configuring global environment variables...");

    if executor.dry_run() {
        println!("[DRY RUN] Creating /etc/profile.d/instantos.sh");
        return Ok(());
    }

    // Ensure directory exists
    std::fs::create_dir_all("/etc/profile.d")?;

    // Resolve EDITOR at install time
    let editor = if std::path::Path::new("/usr/bin/nvim").exists() {
        "/usr/bin/nvim"
    } else if std::path::Path::new("/usr/bin/vim").exists() {
        "/usr/bin/vim"
    } else if std::path::Path::new("/usr/bin/nano").exists() {
        "/usr/bin/nano"
    } else {
        "vi"
    };

    let content = format!(
        r#"# Global environment variables for instantOS
export PAGER=less
export EDITOR={}
export XDG_MENU_PREFIX=gnome-
export _JAVA_AWT_WM_NONREPARENTING=1
"#,
        editor
    );

    std::fs::write("/etc/profile.d/instantos.sh", content)?;

    Ok(())
}

fn sync_repos(executor: &dyn CommandRunner) -> Result<()> {
    println!("Updating package databases...");
    let mut cmd = Command::new("pacman");
    cmd.arg("-Sy");
    executor.run(&mut cmd)?;
    Ok(())
}

fn install_standard_packages(context: &InstallContext, executor: &dyn CommandRunner) -> Result<()> {
    println!("Installing standard packages...");
    let packages = crate::arch::execution::packages::build_standard_package_plan(context)?;
    let package_refs: Vec<&str> = packages.iter().map(|s| s.as_str()).collect();
    super::pacman::install(&package_refs, executor)?;
    Ok(())
}

async fn configure_pacman_target(executor: &dyn CommandRunner) -> Result<()> {
    println!("Configuring target pacman settings...");
    crate::common::pacman::configure_pacman_settings(Some("/etc/pacman.conf"), executor.dry_run())
        .await?;
    Ok(())
}

/// Packages required for configuration steps (installed in a single batch elsewhere)
pub fn config_package_list(context: &InstallContext) -> Vec<String> {
    let mut packages = Vec::new();

    if context.get_answer_bool(QuestionId::UseEncryption) {
        packages.push("lvm2".to_string());
        packages.push("cryptsetup".to_string());
    }

    if context.get_answer_bool(QuestionId::UsePlymouth)
        && !context.get_answer_bool(QuestionId::MinimalMode)
    {
        packages.push("plymouth".to_string());
    }

    packages
}

fn configure_mkinitcpio(context: &InstallContext, executor: &dyn CommandRunner) -> Result<()> {
    let use_encryption = context.get_answer_bool(QuestionId::UseEncryption);
    let use_plymouth = context.get_answer_bool(QuestionId::UsePlymouth);

    if !use_encryption && !use_plymouth {
        return Ok(());
    }

    if use_encryption {
        println!("Configuring mkinitcpio for encryption...");
    }
    if use_plymouth && !context.get_answer_bool(QuestionId::MinimalMode) {
        println!("Configuring mkinitcpio for Plymouth...");
    }

    if executor.dry_run() {
        if use_plymouth && !context.get_answer_bool(QuestionId::MinimalMode) {
            println!("[DRY RUN] Adding 'plymouth' to HOOKS in /etc/mkinitcpio.conf");
        }
        if use_encryption {
            println!("[DRY RUN] Adding 'encrypt lvm2' to HOOKS in /etc/mkinitcpio.conf");
        }
        println!("[DRY RUN] mkinitcpio -P");
        return Ok(());
    }

    let conf_path = "/etc/mkinitcpio.conf";
    let content = std::fs::read_to_string(conf_path).context("Failed to read mkinitcpio.conf")?;

    let mut config = MkinitcpioConfig::parse(&content)?;

    // Switch to systemd hooks
    if config.contains_hook("base") && config.contains_hook("udev") {
        config.replace_hook("udev", "systemd");
    }

    // Plymouth should be after systemd but before encrypt/sd-encrypt
    // And definitely before sd-encrypt to show password prompt
    if use_plymouth && !context.get_answer_bool(QuestionId::MinimalMode) {
        config.ensure_hook_position(
            "plymouth",
            &["base", "systemd", "udev"],       // After these
            &["sd-encrypt", "encrypt", "lvm2"], // Before these
        );
    }

    // Ensure keyboard and keymap/sd-vconsole are present
    if !config.contains_hook("sd-vconsole") {
        if config.contains_hook("keymap") {
            config.replace_hook("keymap", "sd-vconsole");
        } else {
            // Ensure sd-vconsole comes after keyboard (or just add it if keyboard not present)
            config.ensure_hook_position("sd-vconsole", &["keyboard"], &[]);
        }
    }

    // Remove consolefont if present as sd-vconsole handles it
    config.remove_hook("consolefont");

    // Add encryption hooks in correct order: block -> sd-encrypt -> lvm2 -> resume -> filesystems
    if use_encryption && config.contains_hook("block") && config.contains_hook("filesystems") {
        // Replace legacy encrypt hook if present
        if config.contains_hook("encrypt") {
            config.replace_hook("encrypt", "sd-encrypt");
        } else {
            config.ensure_hook("sd-encrypt");
        }

        // Ensure correct ordering for full disk encryption with LVM and resume
        config.ensure_hook_position("sd-encrypt", &["block"], &["filesystems"]);
        config.ensure_hook_position("lvm2", &["sd-encrypt"], &["filesystems"]);
        config.ensure_hook_position("resume", &["lvm2"], &["filesystems"]);
    }

    std::fs::write(conf_path, config.to_string())?;

    // Regenerate initramfs
    executor.run(Command::new("mkinitcpio").arg("-P"))?;

    Ok(())
}

fn configure_vconsole(context: &InstallContext, executor: &dyn CommandRunner) -> Result<()> {
    let keymap = context
        .get_answer(&QuestionId::Keymap)
        .context("Keymap not selected")?;

    println!("Setting console keymap to {}", keymap);

    if executor.dry_run() {
        println!("[DRY RUN] echo 'KEYMAP={}' > /etc/vconsole.conf", keymap);
    } else {
        std::fs::write("/etc/vconsole.conf", format!("KEYMAP={}\n", keymap))?;
    }

    Ok(())
}

fn configure_timezone(context: &InstallContext, executor: &dyn CommandRunner) -> Result<()> {
    let timezone = context
        .get_answer(&QuestionId::Timezone)
        .context("Timezone not selected")?;

    println!("Setting timezone to {}", timezone);

    // Try timedatectl first
    // timedatectl set-timezone "$REGION"
    let mut cmd = Command::new("timedatectl");
    cmd.arg("set-timezone").arg(timezone);

    // We try to run timedatectl. If it fails (e.g. in chroot without dbus), we fallback.
    // We suppress the error from executor.run by checking the result.
    if executor.run(&mut cmd).is_ok() {
        // timedatectl set-ntp true
        let mut cmd_ntp = Command::new("timedatectl");
        cmd_ntp.arg("set-ntp").arg("true");
        // We ignore errors here as NTP might not be controllable in chroot
        let _ = executor.run(&mut cmd_ntp);
    } else {
        println!("timedatectl failed, falling back to manual configuration...");

        // ln -sf /usr/share/zoneinfo/Region/City /etc/localtime
        let source = format!("/usr/share/zoneinfo/{}", timezone);
        let target = "/etc/localtime";

        if executor.dry_run() {
            println!("[DRY RUN] ln -sf {} {}", source, target);
        } else {
            // Remove existing link/file if it exists to avoid error
            if std::path::Path::new(target).exists() {
                std::fs::remove_file(target)?;
            }
            std::os::unix::fs::symlink(&source, target)?;
        }

        // hwclock --systohc
        let mut cmd_hw = Command::new("hwclock");
        cmd_hw.arg("--systohc");
        executor.run(&mut cmd_hw)?;
    }

    Ok(())
}

fn configure_locale(context: &InstallContext, executor: &dyn CommandRunner) -> Result<()> {
    let locale = context
        .get_answer(&QuestionId::Locale)
        .context("Locale not selected")?;

    println!("Setting locale to {}", locale);

    if executor.dry_run() {
        println!("[DRY RUN] Uncommenting {} in /etc/locale.gen", locale);
        println!("[DRY RUN] locale-gen");
        // Extract just the LANG part, e.g., "en_US.UTF-8" from "en_US.UTF-8 UTF-8"
        let lang = locale.split_whitespace().next().unwrap_or(locale);
        println!("[DRY RUN] localectl set-locale LANG={}", lang);
    } else {
        // Read /etc/locale.gen
        let locale_gen_path = "/etc/locale.gen";
        let content =
            std::fs::read_to_string(locale_gen_path).context("Failed to read /etc/locale.gen")?;

        // Uncomment the selected locale
        let mut new_lines = Vec::new();
        let mut found = false;

        for line in content.lines() {
            if line.contains(locale) && line.trim().starts_with('#') {
                // Uncomment it
                new_lines.push(line.replacen('#', "", 1));
                found = true;
            } else if line.contains(locale) && !line.trim().starts_with('#') {
                // Already uncommented
                new_lines.push(line.to_string());
                found = true;
            } else {
                new_lines.push(line.to_string());
            }
        }

        if !found {
            // Append it if not found
            new_lines.push(locale.clone());
        }

        std::fs::write(locale_gen_path, new_lines.join("\n"))?;

        // Run locale-gen
        let mut cmd = Command::new("locale-gen");
        executor.run(&mut cmd)?;

        // Use localectl to set the system locale instead of directly editing /etc/locale.conf
        // Extract just the LANG part, e.g., "en_US.UTF-8" from "en_US.UTF-8 UTF-8"
        let lang = locale.split_whitespace().next().unwrap_or(locale);
        let mut cmd = Command::new("localectl");
        cmd.arg("set-locale").arg(format!("LANG={}", lang));
        executor.run(&mut cmd)?;
    }

    Ok(())
}

fn configure_network(context: &InstallContext, executor: &dyn CommandRunner) -> Result<()> {
    let hostname = context
        .get_answer(&QuestionId::Hostname)
        .context("Hostname not set")?;

    println!("Setting hostname to {}", hostname);

    if executor.dry_run() {
        println!("[DRY RUN] echo '{}' > /etc/hostname", hostname);
        println!("[DRY RUN] Writing /etc/hosts");
    } else {
        std::fs::write("/etc/hostname", format!("{}\n", hostname))?;

        let hosts_content = format!(
            "127.0.0.1\tlocalhost\n::1\t\tlocalhost\n127.0.1.1\t{}.localdomain\t{}\n",
            hostname, hostname
        );
        std::fs::write("/etc/hosts", hosts_content)?;
    }

    Ok(())
}

fn configure_users(context: &InstallContext, executor: &dyn CommandRunner) -> Result<()> {
    let username = context
        .get_answer(&QuestionId::Username)
        .context("Username not set")?;
    let password = context
        .get_answer(&QuestionId::Password)
        .context("Password not set")?;

    println!("Configuring user: {}", username);

    // Set root password
    // echo "root:password" | chpasswd
    let root_input = format!("root:{}", password);
    let mut cmd_root = Command::new("chpasswd");
    executor.run_with_input(&mut cmd_root, &root_input)?;

    // Ensure all required groups exist
    ensure_groups_exist(executor)?;

    // Check if user already exists (idempotent)
    let user_exists = if executor.dry_run() {
        false // In dry-run, always show what would happen
    } else {
        Command::new("id")
            .arg(username)
            .stdout(std::process::Stdio::null())
            .stderr(std::process::Stdio::null())
            .status()
            .map(|s| s.success())
            .unwrap_or(false)
    };

    if user_exists {
        println!(
            "User {} already exists, ensuring group membership and shell...",
            username
        );
        // Add user to groups if not already a member
        add_user_to_groups(username, executor)?;

        // Ensure shell is zsh
        let mut cmd_chsh = Command::new("chsh");
        cmd_chsh.arg("-s").arg("/bin/zsh").arg(username);
        executor.run(&mut cmd_chsh)?;
    } else {
        let shell = "/bin/zsh";

        let mut cmd_user = Command::new("useradd");
        cmd_user
            .arg("-m")
            .arg("-G")
            .arg(USER_GROUPS.join(","))
            .arg("-s")
            .arg(shell)
            .arg(username);

        executor.run(&mut cmd_user)?;
    }

    // Set user password
    let user_input = format!("{}:{}", username, password);
    let mut cmd_pass = Command::new("chpasswd");
    executor.run_with_input(&mut cmd_pass, &user_input)?;

    Ok(())
}

pub fn configure_sudo(_context: &InstallContext, executor: &dyn CommandRunner) -> Result<()> {
    println!("Configuring sudoers...");
    // Uncomment %wheel ALL=(ALL:ALL) ALL

    if executor.dry_run() {
        println!("[DRY RUN] Uncommenting %wheel in /etc/sudoers");
        println!("[DRY RUN] Adding 'Defaults env_reset,pwfeedback' to /etc/sudoers");
    } else {
        let sudoers_path = "/etc/sudoers";
        let content =
            std::fs::read_to_string(sudoers_path).context("Failed to read /etc/sudoers")?;

        let mut new_lines = Vec::new();
        let mut has_pwfeedback = false; // Track if pwfeedback already exists

        for line in content.lines() {
            // Check if pwfeedback line already exists
            if line.contains("Defaults") && line.contains("pwfeedback") {
                has_pwfeedback = true;
            }

            if line.contains("%wheel ALL=(ALL:ALL) ALL") && line.trim().starts_with('#') {
                new_lines.push(line.replacen('#', "", 1).trim().to_string());
            } else {
                new_lines.push(line.to_string());
            }
        }

        // Add defaults only if not present (FIXED: Check has_pwfeedback first)
        if !has_pwfeedback {
            new_lines.push("Defaults env_reset,pwfeedback".to_string());
        }

        std::fs::write(sudoers_path, new_lines.join("\n"))?;
    }

    Ok(())
}

pub fn configure_plymouth(context: &InstallContext, executor: &dyn CommandRunner) -> Result<()> {
    if !context.get_answer_bool(QuestionId::UsePlymouth)
        || context.get_answer_bool(QuestionId::MinimalMode)
    {
        return Ok(());
    }

    println!("Configuring Plymouth...");

    if executor.dry_run() {
        println!("[DRY RUN] Setting Plymouth theme to instantos");
        println!("[DRY RUN] plymouth-set-default-theme -R instantos");
        return Ok(());
    }

    let theme = "instantos";
    let theme_dir = format!("/usr/share/plymouth/themes/{}", theme);
    if !std::path::Path::new(&theme_dir).exists() {
        println!(
            "Warning: Plymouth theme '{}' not found at {}. Skipping theme apply.",
            theme, theme_dir
        );
        return Ok(());
    }

    // Configure Plymouth theme
    let plymouth_conf = "/etc/plymouth/plymouthd.conf";

    // Create Plymouth config directory if it doesn't exist
    if let Err(e) = std::fs::create_dir_all("/etc/plymouth") {
        println!("Warning: Failed to create /etc/plymouth: {}", e);
        return Ok(());
    }

    // Note: If encryption is used, the Plymouth theme will not be visible during the
    // password prompt because the theme files are on the encrypted partition.
    let config_content = format!("[Daemon]\nTheme={}\nShowDelay=0\n", theme);

    if let Err(e) = std::fs::write(plymouth_conf, config_content) {
        println!(
            "Warning: Failed to write /etc/plymouth/plymouthd.conf: {}",
            e
        );
        return Ok(());
    }

    // Set the theme and rebuild initramfs
    let mut cmd = Command::new("plymouth-set-default-theme");
    cmd.arg("-R").arg(theme);
    if let Err(e) = executor.run(&mut cmd) {
        println!("Warning: Failed to apply Plymouth theme: {}", e);
        return Ok(());
    }

    Ok(())
}

#[cfg(test)]
mod tests {
    use crate::arch::execution::mock::MockRunner;

    #[test]
    fn test_ensure_groups_exist_commands() {
        let mock = MockRunner::new();
        super::ensure_groups_exist(&mock).unwrap();

        let log = mock.command_log();
        // Should have groupadd commands for each user group + system groups
        assert!(
            log.iter()
                .all(|c| c.contains("groupadd") && c.contains("-f"))
        );
        // USER_GROUPS: wheel, video, docker, sys, rfkill
        assert!(log.iter().any(|c| c.contains("wheel")));
        assert!(log.iter().any(|c| c.contains("video")));
        assert!(log.iter().any(|c| c.contains("docker")));
        assert!(log.iter().any(|c| c.contains("sys")));
        assert!(log.iter().any(|c| c.contains("rfkill")));
        // SYSTEM_GROUPS: nobody
        assert!(log.iter().any(|c| c.contains("nobody")));
        // Total: 5 user groups + 1 system group = 6
        assert_eq!(log.len(), 6);
    }

    #[test]
    fn test_add_user_to_groups_commands() {
        let mock = MockRunner::new();
        super::add_user_to_groups("testuser", &mock).unwrap();

        let log = mock.command_log();
        assert_eq!(log.len(), 1);
        assert!(log[0].contains("usermod"));
        assert!(log[0].contains("-aG"));
        assert!(log[0].contains("testuser"));
    }
}
