//! System settings
//!
//! System administration, updates, and firmware settings.

use anyhow::{Context, Result, bail};
use duct::cmd;
use std::path::Path;
use std::process::Command;

use crate::common::distro::OperatingSystem;
use crate::common::format::format_size;
use crate::menu_utils::{FzfWrapper, TextEditOutcome, TextEditPrompt, prompt_text_edit};
use crate::settings::context::SettingsContext;
use crate::settings::deps::{FASTFETCH, GNOME_FIRMWARE, PACMAN_CONTRIB, TOPGRADE};
use crate::settings::setting::{Setting, SettingMetadata, SettingState, SettingType};
use crate::settings::store::{BoolSettingKey, PACMAN_AUTOCLEAN_KEY};
use crate::ui::prelude::*;

// ============================================================================
// Hostname
// ============================================================================

pub struct Hostname;

impl Setting for Hostname {
    fn metadata(&self) -> SettingMetadata {
        SettingMetadata::builder()
            .id("system.hostname")
            .title("Hostname")
            .icon(NerdFont::Tag)
            .summary("Set the system hostname via hostnamectl.\n\nThe hostname identifies this machine on the network.")
            .search_keywords(&["name", "computer", "machine"])
            .build()
    }

    fn setting_type(&self) -> SettingType {
        SettingType::Action
    }

    fn get_display_state(&self, _ctx: &SettingsContext) -> SettingState {
        let current = current_hostname().unwrap_or_default();
        SettingState::Choice {
            current_label: current.unwrap_or_else(|| "Not set".to_string()),
        }
    }

    fn apply(&self, ctx: &mut SettingsContext) -> Result<()> {
        configure_hostname(ctx)
    }
}

fn current_hostname() -> Result<Option<String>> {
    let output = Command::new("hostnamectl")
        .args(["hostname"])
        .output()
        .context("running hostnamectl hostname")?;

    if !output.status.success() {
        return Ok(None);
    }

    Ok(String::from_utf8_lossy(&output.stdout)
        .trim()
        .to_string()
        .into())
}

fn validate_hostname(name: &str) -> Result<()> {
    if name.is_empty() {
        bail!("hostname cannot be empty");
    }
    if name.len() > 63 {
        bail!("hostname must be 63 characters or fewer");
    }
    if name.starts_with('-') || name.ends_with('-') {
        bail!("hostname cannot start or end with a hyphen");
    }
    if !name.chars().all(|c| c.is_ascii_alphanumeric() || c == '-') {
        bail!("hostname may only contain ASCII letters, digits, and hyphens");
    }
    Ok(())
}

fn configure_hostname(ctx: &mut SettingsContext) -> Result<()> {
    if which::which("hostnamectl").is_err() {
        ctx.emit_unsupported(
            "settings.hostname.no_systemd",
            "Hostname configuration requires systemd (hostnamectl not found).",
        );
        return Ok(());
    }

    let current = current_hostname()?.unwrap_or_default();

    let outcome = prompt_text_edit(
        TextEditPrompt::new(
            "Hostname",
            if current.is_empty() {
                None
            } else {
                Some(&current)
            },
        )
        .header("Enter the new hostname for this machine")
        .ghost("e.g. my-laptop"),
    )?;

    match outcome {
        TextEditOutcome::Updated(Some(ref new_hostname)) => {
            if let Err(e) = validate_hostname(new_hostname) {
                ctx.emit_failure(
                    "settings.hostname.invalid",
                    &format!("Invalid hostname: {e}"),
                );
                return Ok(());
            }

            ctx.run_command_as_root("hostnamectl", ["set-hostname", new_hostname.as_str()])?;
            handle_cloudinit_hostname(ctx)?;
            ctx.emit_success(
                "settings.hostname.updated",
                &format!("Hostname set to {new_hostname}."),
            );
            ctx.notify("Hostname", &format!("Hostname changed to {new_hostname}."));
        }
        TextEditOutcome::Unchanged => {
            ctx.emit_info("settings.hostname.unchanged", "Hostname unchanged.");
        }
        TextEditOutcome::Updated(None) | TextEditOutcome::Cancelled => {}
    }

    Ok(())
}

const CLOUDINIT_CFG: &str = "/etc/cloud/cloud.cfg";
const CLOUDINIT_DROPIN: &str = "/etc/cloud/cloud.cfg.d/99-preserve-hostname.cfg";

/// Check if cloud-init manages the hostname and offer to disable that.
fn handle_cloudinit_hostname(ctx: &mut SettingsContext) -> Result<()> {
    if !Path::new(CLOUDINIT_CFG).exists() {
        return Ok(());
    }

    // Already preserved via drop-in?
    if Path::new(CLOUDINIT_DROPIN).exists()
        && let Ok(content) = std::fs::read_to_string(CLOUDINIT_DROPIN)
        && content.contains("preserve_hostname: true")
    {
        return Ok(());
    }

    // Check main config
    if let Ok(content) = std::fs::read_to_string(CLOUDINIT_CFG)
        && content
            .lines()
            .any(|l| l.trim() == "preserve_hostname: true")
    {
        return Ok(());
    }

    let result = FzfWrapper::builder()
        .confirm(
            "cloud-init detected.\n\n\
             cloud-init may overwrite the hostname on next boot.\n\
             Write preserve_hostname: true to prevent this?",
        )
        .yes_text("Preserve hostname")
        .no_text("Skip")
        .confirm_dialog()?;

    if matches!(result, crate::menu_utils::ConfirmResult::Yes) {
        ctx.run_command_as_root(
            "sh",
            [
                "-c",
                &format!(
                    "mkdir -p /etc/cloud/cloud.cfg.d && echo 'preserve_hostname: true' > {CLOUDINIT_DROPIN}"
                ),
            ],
        )?;
        ctx.emit_success(
            "settings.hostname.cloudinit",
            "cloud-init hostname preservation enabled.",
        );
    }

    Ok(())
}

// ============================================================================
// About System (uses shell command with read, can't use macro)
// ============================================================================

pub struct AboutSystem;

impl Setting for AboutSystem {
    fn metadata(&self) -> SettingMetadata {
        SettingMetadata::builder()
            .id("system.about")
            .title("About")
            .icon(NerdFont::About)
            .summary("Display system information using fastfetch.")
            .requirements(vec![&FASTFETCH])
            .search_keywords(&["fastfetch", "neofetch"])
            .build()
    }

    fn setting_type(&self) -> SettingType {
        SettingType::Command
    }

    fn preview_command(&self) -> Option<String> {
        Some("fastfetch --pipe false 2>/dev/null || echo 'fastfetch not available'".to_string())
    }

    fn apply(&self, ctx: &mut SettingsContext) -> Result<()> {
        ctx.emit_info(
            "settings.command.launching",
            "Displaying system information...",
        );
        cmd!("sh", "-c", "fastfetch && read -n 1")
            .run()
            .context("running fastfetch")?;
        Ok(())
    }
}

// ============================================================================
// System Doctor (runs ins doctor fix --choose for interactive diagnostics)
// ============================================================================

pub struct SystemDoctor;

impl Setting for SystemDoctor {
    fn metadata(&self) -> SettingMetadata {
        SettingMetadata::builder()
            .id("system.doctor")
            .title("System Diagnostics")
            .icon(NerdFont::ShieldCheck)
            .summary("Run system diagnostics to check for common issues and available fixes.")
            .requirements(vec![&PACMAN_CONTRIB])
            .search_keywords(&["health", "doctor"])
            .build()
    }

    fn setting_type(&self) -> SettingType {
        SettingType::Action
    }

    fn apply(&self, ctx: &mut SettingsContext) -> Result<()> {
        ctx.emit_info(
            "settings.command.launching",
            "Running system diagnostics...",
        );

        // Simply call the new doctor fix --choose command
        // It handles everything: diagnosis, interactive menu, and fixes
        cmd!(env!("CARGO_BIN_NAME"), "doctor", "fix", "--choose")
            .run()
            .context("running interactive doctor fix")?;

        ctx.notify("System Diagnostics", "Diagnostic session completed.");
        Ok(())
    }
}

// ============================================================================
// Dotfile Manager (runs ins dot menu for interactive dotfile management)
// ============================================================================

pub struct DotfileManager;

impl Setting for DotfileManager {
    fn metadata(&self) -> SettingMetadata {
        SettingMetadata::builder()
            .id("system.dotfiles")
            .title("Dotfile Manager")
            .icon(NerdFont::File)
            .summary("Manage dotfile repositories, subdirectories, and file sources.")
            .build()
    }

    fn setting_type(&self) -> SettingType {
        SettingType::Action
    }

    fn apply(&self, ctx: &mut SettingsContext) -> Result<()> {
        ctx.emit_info("settings.command.launching", "Opening dotfile manager...");

        cmd!(env!("CARGO_BIN_NAME"), "dot", "menu")
            .run()
            .context("running dotfile menu")?;

        Ok(())
    }
}

// ============================================================================
// Systemd Manager (fzf-based menu for managing services)
// ============================================================================

pub struct SystemdManager;

impl Setting for SystemdManager {
    fn metadata(&self) -> SettingMetadata {
        SettingMetadata::builder()
            .id("system.systemd")
            .title("Systemd Services")
            .icon(NerdFont::Server)
            .summary("Manage systemd services: start, stop, enable, disable, and view logs.")
            .build()
    }

    fn setting_type(&self) -> SettingType {
        SettingType::Action
    }

    fn apply(&self, _ctx: &mut SettingsContext) -> Result<()> {
        crate::settings::systemd::run_systemd_menu()
    }
}

// ============================================================================
// Web UI (Cockpit) - Launch Cockpit web interface
// ============================================================================

pub struct WebUiManager;

impl Setting for WebUiManager {
    fn metadata(&self) -> SettingMetadata {
        SettingMetadata::builder()
            .id("system.webui")
            .title("Web UI (Cockpit)")
            .icon(NerdFont::Globe)
            .summary("Launch Cockpit web interface for managing systemd services, logs, and system resources via browser.")
            .build()
    }

    fn setting_type(&self) -> SettingType {
        SettingType::Action
    }

    fn apply(&self, _ctx: &mut SettingsContext) -> Result<()> {
        crate::settings::systemd::launch_cockpit()
    }
}

// ============================================================================
// Password Manager (launches the pass edit browser)
// ============================================================================

pub struct PasswordManager;

impl Setting for PasswordManager {
    fn metadata(&self) -> SettingMetadata {
        SettingMetadata::builder()
            .id("system.password_manager")
            .title("Password Manager")
            .icon(NerdFont::Key)
            .summary("Browse, edit, add, and delete password store entries.\n\nOpens the interactive pass store browser for managing passwords and OTP entries.")
            .search_keywords(&["pass", "credentials", "secrets", "keychain", "otp"])
            .build()
    }

    fn setting_type(&self) -> SettingType {
        SettingType::Action
    }

    fn apply(&self, _ctx: &mut SettingsContext) -> Result<()> {
        crate::pass::menu::run_edit_browser(None)
    }
}

// ============================================================================
// Firmware Manager (GUI app)
// ============================================================================

gui_command_setting!(
    FirmwareManager,
    "system.firmware",
    "Firmware Manager",
    NerdFont::Cpu,
    "Launch GNOME Firmware manager to view and update device firmware.\n\nManage firmware for BIOS/UEFI, devices, and peripherals.",
    "gnome-firmware",
    &GNOME_FIRMWARE
);

// ============================================================================
// System Upgrade (TUI app)
// ============================================================================

tui_command_setting!(
    SystemUpgrade,
    "system.upgrade",
    "Upgrade",
    NerdFont::Upgrade,
    "Upgrade all installed packages and system components using topgrade.",
    "topgrade",
    &TOPGRADE,
    &["update"]
);

// ============================================================================
// Pacman Cache Autoclean
// ============================================================================

pub struct PacmanAutoclean;

impl PacmanAutoclean {
    const KEY: BoolSettingKey = BoolSettingKey::new("system.pacman_autoclean", false);
}

impl Setting for PacmanAutoclean {
    fn metadata(&self) -> SettingMetadata {
        SettingMetadata::builder()
            .id("system.pacman_autoclean")
            .title("Pacman cache autoclean")
            .icon(NerdFont::Trash)
            .summary("Run paccache weekly to keep only the latest pacman packages.")
            .requirements(vec![&PACMAN_CONTRIB])
            .supported_distros(&[OperatingSystem::Arch])
            .build()
    }

    fn setting_type(&self) -> SettingType {
        SettingType::Toggle { key: Self::KEY }
    }

    fn apply(&self, ctx: &mut SettingsContext) -> Result<()> {
        let current = ctx.bool(Self::KEY);
        let target = !current;
        ctx.set_bool(Self::KEY, target);
        apply_pacman_autoclean(ctx, target)
    }
}

// ============================================================================
// Clear Pacman Cache
// ============================================================================

pub struct ClearPacmanCache;

impl Setting for ClearPacmanCache {
    fn metadata(&self) -> SettingMetadata {
        SettingMetadata::builder()
            .id("system.pacman_cache_clear")
            .title("Clear pacman cache")
            .icon(NerdFont::Trash)
            .summary("Remove all cached pacman packages using pacman -Scc.")
            .supported_distros(&[OperatingSystem::Arch])
            .build()
    }

    fn setting_type(&self) -> SettingType {
        SettingType::Action
    }

    fn apply(&self, ctx: &mut SettingsContext) -> Result<()> {
        let cache_size = describe_pacman_cache_size();
        let message = format!("Clear pacman cache?\n\nCache size: {cache_size}");

        let result = FzfWrapper::builder()
            .confirm(message)
            .yes_text("Clear cache")
            .no_text("Keep cache")
            .confirm_dialog()?;

        if matches!(result, crate::menu_utils::ConfirmResult::Yes) {
            ctx.emit_info("settings.pacman_cache.clearing", "Clearing pacman cache...");
            ctx.run_command_as_root("pacman", ["-Scc"])?;
            ctx.notify("Pacman cache", "Pacman cache cleared.");
        }

        Ok(())
    }
}

// Note: PacmanAutoclean cannot use simple_toggle_setting! macro because it has
// custom apply logic (calls apply_pacman_autoclean) and requirements that need
// to be checked. The simple_toggle_setting! macro is only for toggles with no
// additional logic beyond flipping the value and showing a message.

// ============================================================================
// Welcome App Autostart
// ============================================================================

simple_toggle_setting!(
    WelcomeAutostart,
    "system.welcome_autostart",
    "Welcome app on startup",
    NerdFont::Home,
    "Show the welcome application automatically when logging in.\n\nThe welcome app provides quick access to the instantOS website and system settings.",
    true,
    "Welcome app will appear on next startup",
    "Welcome app autostart has been disabled"
);

// ============================================================================
// Implementations
// ============================================================================

pub fn apply_pacman_autoclean(ctx: &mut SettingsContext, _enabled: bool) -> Result<()> {
    // set_bool already applied the change to the external source (if one exists)
    // or persisted to the store (if no external source).
    // Just refresh to get the current state for the notification.
    let active = ctx.refresh_bool_source(PACMAN_AUTOCLEAN_KEY)?;

    if active {
        ctx.notify(
            "Pacman cache",
            "Automatic weekly pacman cache cleanup enabled.",
        );
    } else {
        ctx.notify("Pacman cache", "Automatic pacman cache cleanup disabled.");
    }

    Ok(())
}

const PACMAN_CACHE_DIR: &str = "/var/cache/pacman/pkg";

fn describe_pacman_cache_size() -> String {
    match calculate_dir_size(PACMAN_CACHE_DIR) {
        Ok(size) => format_size(size),
        Err(_) => "Unknown".to_string(),
    }
}

fn calculate_dir_size(path: &str) -> Result<u64> {
    let cache_path = std::path::Path::new(path);
    if !cache_path.exists() {
        return Ok(0);
    }

    let mut total_size: u64 = 0;
    let mut dirs_to_visit = vec![cache_path.to_path_buf()];

    while let Some(dir) = dirs_to_visit.pop() {
        let entries = match std::fs::read_dir(&dir) {
            Ok(entries) => entries,
            Err(_) => continue,
        };

        for entry in entries {
            let entry = match entry {
                Ok(entry) => entry,
                Err(_) => continue,
            };

            let metadata = match entry.metadata() {
                Ok(metadata) => metadata,
                Err(_) => continue,
            };

            if metadata.is_file() {
                total_size += metadata.len();
            } else if metadata.is_dir() {
                dirs_to_visit.push(entry.path());
            }
        }
    }

    Ok(total_size)
}
