use serde_json::{Value, json};
use std::env;
use std::process::Output;
use tokio::process::Command;

use super::error::ProvisioningError;
use super::inspect_docker_runtime_access_current_user;
use super::types::{
    InstallLocalProvisionDependenciesParams, LocalProvisionDependencyInstallResult,
    LocalProvisionDependencyInstallStep, LocalProvisionDependencyStatus,
};

#[derive(Debug, Clone, Copy)]
enum PackageManager {
    Apt,
    Dnf,
    Yum,
    Apk,
    Pacman,
    Zypper,
}

impl PackageManager {
    fn as_str(self) -> &'static str {
        match self {
            Self::Apt => "apt-get",
            Self::Dnf => "dnf",
            Self::Yum => "yum",
            Self::Apk => "apk",
            Self::Pacman => "pacman",
            Self::Zypper => "zypper",
        }
    }
}

struct CommandSpec {
    key: &'static str,
    description: String,
    program: String,
    args: Vec<String>,
    envs: Vec<(String, String)>,
}

pub async fn inspect_local_provisioning_dependencies() -> LocalProvisionDependencyStatus {
    let package_manager_detected = detect_package_manager();
    let package_manager = package_manager_detected.map(|manager| manager.as_str().to_string());
    let running_as_root = detect_running_as_root().await;
    let sudo_available = binary_on_path("sudo");
    let sudo_non_interactive_available =
        detect_sudo_non_interactive_available(running_as_root, sudo_available).await;
    let systemctl_available = binary_on_path("systemctl");
    let docker_binary_available = binary_on_path("docker");
    let postgres_binary_available = binary_on_path("psql");
    let docker_runtime_access = inspect_docker_runtime_access_current_user().await;
    let docker_service_active = if systemctl_available && docker_binary_available {
        service_active("docker").await
    } else {
        None
    };
    let postgres_service_active = if systemctl_available {
        service_active("postgresql").await
    } else {
        None
    };

    let mut missing = Vec::new();
    let mut notes = Vec::new();

    if !cfg!(target_os = "linux") {
        notes.push(
            "Automatic dependency installation is only supported on Linux hosts.".to_string(),
        );
    }
    if package_manager.is_none() && cfg!(target_os = "linux") {
        notes.push("No supported package manager was detected on PATH.".to_string());
    }
    if !docker_binary_available {
        missing.push("docker".to_string());
    } else if !docker_runtime_access.available {
        notes.push(format!(
            "Docker runtime check: {}",
            docker_runtime_access.detail
        ));
    }
    if !postgres_binary_available {
        missing.push("postgres".to_string());
    }
    if !running_as_root && !sudo_available && cfg!(target_os = "linux") {
        notes.push("Package installation requires root or passwordless sudo.".to_string());
    } else if !running_as_root
        && sudo_available
        && !sudo_non_interactive_available
        && cfg!(target_os = "linux")
    {
        notes.push(
            "sudo is installed but requires a password; non-interactive installs are unavailable."
                .to_string(),
        );
    }

    LocalProvisionDependencyStatus {
        os_family: env::consts::OS.to_string(),
        package_manager,
        running_as_root,
        sudo_available,
        sudo_non_interactive_available,
        systemctl_available,
        can_attempt_install: compute_can_attempt_install(
            cfg!(target_os = "linux"),
            package_manager_detected.is_some(),
            running_as_root,
            sudo_non_interactive_available,
        ),
        docker_binary_available,
        docker_service_active,
        docker_runtime_access,
        postgres_binary_available,
        postgres_service_active,
        missing,
        notes,
    }
}

pub async fn install_local_provisioning_dependencies(
    params: InstallLocalProvisionDependenciesParams,
) -> Result<LocalProvisionDependencyInstallResult, ProvisioningError> {
    if !params.install_docker && !params.install_postgres {
        return Err(ProvisioningError::InvalidInput(
            "At least one install target must be enabled.".to_string(),
        ));
    }

    let before = inspect_local_provisioning_dependencies().await;
    if before.os_family != "linux" {
        return Err(ProvisioningError::Unavailable(
            "Automatic dependency installation is only supported on Linux hosts.".to_string(),
        ));
    }

    let package_manager = detect_package_manager().ok_or_else(|| {
        ProvisioningError::Unavailable(
            "No supported package manager was detected on PATH.".to_string(),
        )
    })?;

    let use_sudo_prefix = build_sudo_prefix(&before, params.use_sudo)?;
    let mut steps = Vec::new();

    let docker_missing = params.install_docker && !before.docker_binary_available;
    let postgres_missing = params.install_postgres && !before.postgres_binary_available;

    if !docker_missing {
        steps.push(LocalProvisionDependencyInstallStep {
            key: "docker_package_install".to_string(),
            status: "skipped".to_string(),
            description: "Docker is already available on PATH.".to_string(),
            command: None,
            detail: Some("Skipped Docker package installation.".to_string()),
            output: None,
        });
    }
    if !postgres_missing {
        steps.push(LocalProvisionDependencyInstallStep {
            key: "postgres_package_install".to_string(),
            status: "skipped".to_string(),
            description: "Postgres client tools are already available on PATH.".to_string(),
            command: None,
            detail: Some("Skipped Postgres package installation.".to_string()),
            output: None,
        });
    }

    let missing_packages = install_package_list(package_manager, docker_missing, postgres_missing);

    if !missing_packages.is_empty() {
        if let Some(command) = package_update_command(package_manager, &use_sudo_prefix) {
            steps.push(run_install_step(command).await);
        }

        let install_command =
            package_install_command(package_manager, &missing_packages, &use_sudo_prefix);
        steps.push(run_install_step(install_command).await);
    }

    if params.start_services {
        if params.install_docker {
            run_optional_service_start(
                "docker_service_start",
                "Attempt to enable and start the Docker service.",
                "docker",
                &before,
                &use_sudo_prefix,
                &mut steps,
            )
            .await;
        }

        if params.install_postgres {
            run_optional_service_start(
                "postgres_service_start",
                "Attempt to enable and start the PostgreSQL service.",
                "postgresql",
                &before,
                &use_sudo_prefix,
                &mut steps,
            )
            .await;
        }
    }

    let after = inspect_local_provisioning_dependencies().await;
    let overall_status = compute_overall_status(&params, &after, &steps);

    Ok(LocalProvisionDependencyInstallResult {
        requested: std::collections::HashMap::from([
            (
                "install_docker".to_string(),
                Value::Bool(params.install_docker),
            ),
            (
                "install_postgres".to_string(),
                Value::Bool(params.install_postgres),
            ),
            (
                "start_services".to_string(),
                Value::Bool(params.start_services),
            ),
            ("use_sudo".to_string(), Value::Bool(params.use_sudo)),
            (
                "package_manager".to_string(),
                json!(package_manager.as_str().to_string()),
            ),
        ]),
        before,
        after,
        overall_status: overall_status.to_string(),
        steps,
    })
}

fn build_sudo_prefix(
    status: &LocalProvisionDependencyStatus,
    use_sudo: bool,
) -> Result<Vec<String>, ProvisioningError> {
    if status.running_as_root {
        return Ok(Vec::new());
    }
    if use_sudo && status.sudo_non_interactive_available {
        return Ok(vec!["sudo".to_string(), "-n".to_string()]);
    }
    Err(ProvisioningError::Unavailable(
        "Installing system packages requires root privileges or passwordless sudo.".to_string(),
    ))
}

fn compute_can_attempt_install(
    is_linux: bool,
    package_manager_detected: bool,
    running_as_root: bool,
    sudo_non_interactive_available: bool,
) -> bool {
    is_linux && package_manager_detected && (running_as_root || sudo_non_interactive_available)
}

fn detect_package_manager() -> Option<PackageManager> {
    [
        ("apt-get", PackageManager::Apt),
        ("dnf", PackageManager::Dnf),
        ("yum", PackageManager::Yum),
        ("apk", PackageManager::Apk),
        ("pacman", PackageManager::Pacman),
        ("zypper", PackageManager::Zypper),
    ]
    .into_iter()
    .find_map(|(binary, manager)| binary_on_path(binary).then_some(manager))
}

fn binary_on_path(binary: &str) -> bool {
    let Some(path_value) = env::var_os("PATH") else {
        return false;
    };

    env::split_paths(&path_value).any(|base| {
        let direct = base.join(binary);
        if direct.is_file() {
            return true;
        }

        #[cfg(windows)]
        {
            let exe = base.join(format!("{binary}.exe"));
            if exe.is_file() {
                return true;
            }
        }

        false
    })
}

async fn detect_running_as_root() -> bool {
    #[cfg(unix)]
    {
        let output = Command::new("id").arg("-u").output().await;
        return output
            .ok()
            .filter(|result| result.status.success())
            .map(|result| String::from_utf8_lossy(&result.stdout).trim() == "0")
            .unwrap_or(false);
    }

    #[cfg(not(unix))]
    {
        false
    }
}

async fn detect_sudo_non_interactive_available(
    running_as_root: bool,
    sudo_available: bool,
) -> bool {
    if !cfg!(target_os = "linux") || running_as_root || !sudo_available {
        return false;
    }
    Command::new("sudo")
        .args(["-n", "true"])
        .output()
        .await
        .map(|result| result.status.success())
        .unwrap_or(false)
}

async fn service_active(service_name: &str) -> Option<bool> {
    let output = Command::new("systemctl")
        .args(["is-active", "--quiet", service_name])
        .output()
        .await
        .ok()?;
    Some(output.status.success())
}

fn install_package_list(
    manager: PackageManager,
    install_docker: bool,
    install_postgres: bool,
) -> Vec<String> {
    let mut packages = Vec::new();

    if install_docker {
        packages.extend(match manager {
            PackageManager::Apt => vec!["docker.io"],
            PackageManager::Dnf | PackageManager::Yum => vec!["docker"],
            PackageManager::Apk => vec!["docker"],
            PackageManager::Pacman => vec!["docker"],
            PackageManager::Zypper => vec!["docker"],
        });
    }

    if install_postgres {
        packages.extend(match manager {
            PackageManager::Apt => vec!["postgresql", "postgresql-contrib", "postgresql-client"],
            PackageManager::Dnf | PackageManager::Yum => {
                vec!["postgresql-server", "postgresql", "postgresql-contrib"]
            }
            PackageManager::Apk => vec!["postgresql", "postgresql-client"],
            PackageManager::Pacman => vec!["postgresql"],
            PackageManager::Zypper => vec!["postgresql-server", "postgresql"],
        });
    }

    packages
        .into_iter()
        .map(str::to_string)
        .collect::<std::collections::BTreeSet<_>>()
        .into_iter()
        .collect()
}

fn package_update_command(manager: PackageManager, sudo_prefix: &[String]) -> Option<CommandSpec> {
    match manager {
        PackageManager::Apt => Some(compose_command(
            "package_index_update",
            "Refresh apt package metadata before installing provisioning dependencies.".to_string(),
            sudo_prefix,
            "apt-get",
            vec!["update".to_string()],
            vec![
                ("DEBIAN_FRONTEND".to_string(), "noninteractive".to_string()),
                ("NEEDRESTART_MODE".to_string(), "a".to_string()),
            ],
        )),
        PackageManager::Pacman => Some(compose_command(
            "package_index_update",
            "Refresh pacman package metadata before installing provisioning dependencies."
                .to_string(),
            sudo_prefix,
            "pacman",
            vec!["-Sy".to_string()],
            Vec::new(),
        )),
        _ => None,
    }
}

fn package_install_command(
    manager: PackageManager,
    packages: &[String],
    sudo_prefix: &[String],
) -> CommandSpec {
    match manager {
        PackageManager::Apt => compose_command(
            "package_install",
            format!(
                "Install requested provisioning dependencies via apt-get: {}.",
                packages.join(", ")
            ),
            sudo_prefix,
            "apt-get",
            {
                let mut args = vec![
                    "install".to_string(),
                    "-y".to_string(),
                    "--no-install-recommends".to_string(),
                ];
                args.extend(packages.iter().cloned());
                args
            },
            vec![
                ("DEBIAN_FRONTEND".to_string(), "noninteractive".to_string()),
                ("NEEDRESTART_MODE".to_string(), "a".to_string()),
            ],
        ),
        PackageManager::Dnf => compose_command(
            "package_install",
            format!(
                "Install requested provisioning dependencies via dnf: {}.",
                packages.join(", ")
            ),
            sudo_prefix,
            "dnf",
            {
                let mut args = vec!["install".to_string(), "-y".to_string()];
                args.extend(packages.iter().cloned());
                args
            },
            Vec::new(),
        ),
        PackageManager::Yum => compose_command(
            "package_install",
            format!(
                "Install requested provisioning dependencies via yum: {}.",
                packages.join(", ")
            ),
            sudo_prefix,
            "yum",
            {
                let mut args = vec!["install".to_string(), "-y".to_string()];
                args.extend(packages.iter().cloned());
                args
            },
            Vec::new(),
        ),
        PackageManager::Apk => compose_command(
            "package_install",
            format!(
                "Install requested provisioning dependencies via apk: {}.",
                packages.join(", ")
            ),
            sudo_prefix,
            "apk",
            {
                let mut args = vec!["add".to_string(), "--no-cache".to_string()];
                args.extend(packages.iter().cloned());
                args
            },
            Vec::new(),
        ),
        PackageManager::Pacman => compose_command(
            "package_install",
            format!(
                "Install requested provisioning dependencies via pacman: {}.",
                packages.join(", ")
            ),
            sudo_prefix,
            "pacman",
            {
                let mut args = vec!["-S".to_string(), "--noconfirm".to_string()];
                args.extend(packages.iter().cloned());
                args
            },
            Vec::new(),
        ),
        PackageManager::Zypper => compose_command(
            "package_install",
            format!(
                "Install requested provisioning dependencies via zypper: {}.",
                packages.join(", ")
            ),
            sudo_prefix,
            "zypper",
            {
                let mut args = vec![
                    "--non-interactive".to_string(),
                    "install".to_string(),
                    "-y".to_string(),
                ];
                args.extend(packages.iter().cloned());
                args
            },
            Vec::new(),
        ),
    }
}

fn compose_command(
    key: &'static str,
    description: String,
    sudo_prefix: &[String],
    base_program: &str,
    base_args: Vec<String>,
    envs: Vec<(String, String)>,
) -> CommandSpec {
    if sudo_prefix.is_empty() {
        CommandSpec {
            key,
            description,
            program: base_program.to_string(),
            args: base_args,
            envs,
        }
    } else {
        let mut args = sudo_prefix.iter().skip(1).cloned().collect::<Vec<_>>();
        args.push(base_program.to_string());
        args.extend(base_args);
        CommandSpec {
            key,
            description,
            program: sudo_prefix[0].clone(),
            args,
            envs,
        }
    }
}

async fn run_install_step(command: CommandSpec) -> LocalProvisionDependencyInstallStep {
    let command_line = build_command_string(&command.program, &command.args);
    let output = execute_command(&command).await;

    match output {
        Ok(output) if output.status.success() => LocalProvisionDependencyInstallStep {
            key: command.key.to_string(),
            status: "success".to_string(),
            description: command.description,
            command: Some(command_line),
            detail: Some("Command completed successfully.".to_string()),
            output: trimmed_output(&output),
        },
        Ok(output) => {
            let detail = trimmed_output(&output)
                .unwrap_or_else(|| "Command exited without output.".to_string());
            LocalProvisionDependencyInstallStep {
                key: command.key.to_string(),
                status: "failed".to_string(),
                description: command.description,
                command: Some(command_line),
                detail: Some(detail.clone()),
                output: Some(detail),
            }
        }
        Err(err) => LocalProvisionDependencyInstallStep {
            key: command.key.to_string(),
            status: "failed".to_string(),
            description: command.description,
            command: Some(command_line),
            detail: Some(format!("Failed to execute command: {err}")),
            output: None,
        },
    }
}

async fn run_optional_service_start(
    key: &'static str,
    description: &str,
    service_name: &str,
    before: &LocalProvisionDependencyStatus,
    sudo_prefix: &[String],
    steps: &mut Vec<LocalProvisionDependencyInstallStep>,
) {
    if !before.systemctl_available {
        steps.push(LocalProvisionDependencyInstallStep {
            key: key.to_string(),
            status: "skipped".to_string(),
            description: description.to_string(),
            command: None,
            detail: Some("systemctl is not available on this host.".to_string()),
            output: None,
        });
        return;
    }

    let command = compose_command(
        key,
        description.to_string(),
        sudo_prefix,
        "systemctl",
        vec![
            "enable".to_string(),
            "--now".to_string(),
            service_name.to_string(),
        ],
        Vec::new(),
    );

    let output = execute_command(&command).await;
    match output {
        Ok(result) if result.status.success() => steps.push(LocalProvisionDependencyInstallStep {
            key: key.to_string(),
            status: "success".to_string(),
            description: description.to_string(),
            command: Some(build_command_string(&command.program, &command.args)),
            detail: Some(format!(
                "Service '{}' is enabled and started.",
                service_name
            )),
            output: trimmed_output(&result),
        }),
        Ok(result) => {
            let detail = trimmed_output(&result)
                .unwrap_or_else(|| "Command exited without output.".to_string());
            steps.push(LocalProvisionDependencyInstallStep {
                key: key.to_string(),
                status: "failed".to_string(),
                description: description.to_string(),
                command: Some(build_command_string(&command.program, &command.args)),
                detail: Some(detail.clone()),
                output: Some(detail),
            });
        }
        Err(err) => steps.push(LocalProvisionDependencyInstallStep {
            key: key.to_string(),
            status: "failed".to_string(),
            description: description.to_string(),
            command: Some(build_command_string(&command.program, &command.args)),
            detail: Some(format!("Failed to execute service command: {err}")),
            output: None,
        }),
    }
}

fn install_step_succeeded(steps: &[LocalProvisionDependencyInstallStep], key: &str) -> bool {
    steps
        .iter()
        .rev()
        .find(|step| step.key == key)
        .map(|step| step.status == "success")
        .unwrap_or(false)
}

fn target_ready(
    binary_available: bool,
    service_active: Option<bool>,
    start_services: bool,
    service_step_key: &str,
    steps: &[LocalProvisionDependencyInstallStep],
) -> bool {
    if !binary_available {
        return false;
    }
    if !start_services {
        return true;
    }
    service_active.unwrap_or(false) || install_step_succeeded(steps, service_step_key)
}

fn compute_overall_status(
    params: &InstallLocalProvisionDependenciesParams,
    after: &LocalProvisionDependencyStatus,
    steps: &[LocalProvisionDependencyInstallStep],
) -> &'static str {
    let mut requested_targets = 0usize;
    let mut ready_targets = 0usize;

    if params.install_docker {
        requested_targets += 1;
        if target_ready(
            after.docker_binary_available,
            after.docker_service_active,
            params.start_services,
            "docker_service_start",
            steps,
        ) {
            ready_targets += 1;
        }
    }

    if params.install_postgres {
        requested_targets += 1;
        if target_ready(
            after.postgres_binary_available,
            after.postgres_service_active,
            params.start_services,
            "postgres_service_start",
            steps,
        ) {
            ready_targets += 1;
        }
    }

    if ready_targets == requested_targets {
        "success"
    } else if ready_targets > 0 {
        "partial"
    } else {
        "failed"
    }
}

async fn execute_command(command: &CommandSpec) -> Result<Output, std::io::Error> {
    let mut process = Command::new(&command.program);
    process.args(&command.args);
    for (key, value) in &command.envs {
        process.env(key, value);
    }
    process.output().await
}

fn build_command_string(program: &str, args: &[String]) -> String {
    let mut parts = vec![program.to_string()];
    parts.extend(args.iter().cloned());
    parts.join(" ")
}

fn trimmed_output(output: &Output) -> Option<String> {
    let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
    let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
    let combined = if stderr.is_empty() {
        stdout
    } else if stdout.is_empty() {
        stderr
    } else {
        format!("{stderr}\n{stdout}")
    };

    if combined.is_empty() {
        return None;
    }

    let truncated = if combined.len() > 4000 {
        format!("{}...", &combined[..4000])
    } else {
        combined
    };
    Some(truncated)
}

#[cfg(test)]
mod tests {
    use super::{
        PackageManager, compute_can_attempt_install, compute_overall_status, install_package_list,
    };
    use crate::provisioning::types::{
        InstallLocalProvisionDependenciesParams, LocalProvisionDependencyInstallStep,
        LocalProvisionDependencyStatus,
    };

    fn status(
        docker_binary_available: bool,
        docker_service_active: Option<bool>,
        postgres_binary_available: bool,
        postgres_service_active: Option<bool>,
    ) -> LocalProvisionDependencyStatus {
        LocalProvisionDependencyStatus {
            os_family: "linux".to_string(),
            package_manager: Some("apt-get".to_string()),
            running_as_root: false,
            sudo_available: true,
            sudo_non_interactive_available: true,
            systemctl_available: true,
            can_attempt_install: true,
            docker_binary_available,
            docker_runtime_access: crate::provisioning::DockerRuntimeAccessStatus {
                available: docker_binary_available,
                status: if docker_binary_available {
                    "ready".to_string()
                } else {
                    "binary_missing".to_string()
                },
                detail: if docker_binary_available {
                    "Docker daemon is reachable by the current process user.".to_string()
                } else {
                    "docker binary is not installed or not available in PATH".to_string()
                },
                hint: None,
            },
            docker_service_active,
            postgres_binary_available,
            postgres_service_active,
            missing: Vec::new(),
            notes: Vec::new(),
        }
    }

    #[test]
    fn install_package_list_collects_expected_apt_packages() {
        let packages = install_package_list(PackageManager::Apt, true, true);
        assert!(packages.iter().any(|pkg| pkg == "docker.io"));
        assert!(packages.iter().any(|pkg| pkg == "postgresql"));
        assert!(packages.iter().any(|pkg| pkg == "postgresql-client"));
    }

    #[test]
    fn install_package_list_deduplicates_requested_packages() {
        let packages = install_package_list(PackageManager::Pacman, true, true);
        assert_eq!(
            packages,
            vec!["docker".to_string(), "postgresql".to_string()]
        );
    }

    #[test]
    fn can_attempt_install_requires_root_or_non_interactive_sudo() {
        assert!(compute_can_attempt_install(true, true, true, false));
        assert!(compute_can_attempt_install(true, true, false, true));
        assert!(!compute_can_attempt_install(true, true, false, false));
        assert!(!compute_can_attempt_install(true, false, true, true));
        assert!(!compute_can_attempt_install(false, true, true, true));
    }

    #[test]
    fn overall_status_is_partial_when_a_requested_service_start_fails() {
        let params = InstallLocalProvisionDependenciesParams {
            install_docker: true,
            install_postgres: true,
            start_services: true,
            use_sudo: true,
        };
        let after = status(true, Some(false), true, Some(true));
        let steps = vec![
            LocalProvisionDependencyInstallStep {
                key: "docker_service_start".to_string(),
                status: "failed".to_string(),
                description: "docker failed".to_string(),
                command: None,
                detail: None,
                output: None,
            },
            LocalProvisionDependencyInstallStep {
                key: "postgres_service_start".to_string(),
                status: "success".to_string(),
                description: "postgres started".to_string(),
                command: None,
                detail: None,
                output: None,
            },
        ];

        assert_eq!(compute_overall_status(&params, &after, &steps), "partial");
    }

    #[test]
    fn overall_status_is_success_when_all_requested_targets_are_ready() {
        let params = InstallLocalProvisionDependenciesParams {
            install_docker: true,
            install_postgres: true,
            start_services: true,
            use_sudo: true,
        };
        let after = status(true, Some(true), true, Some(true));
        let steps = vec![
            LocalProvisionDependencyInstallStep {
                key: "docker_service_start".to_string(),
                status: "success".to_string(),
                description: "docker started".to_string(),
                command: None,
                detail: None,
                output: None,
            },
            LocalProvisionDependencyInstallStep {
                key: "postgres_service_start".to_string(),
                status: "success".to_string(),
                description: "postgres started".to_string(),
                command: None,
                detail: None,
                output: None,
            },
        ];

        assert_eq!(compute_overall_status(&params, &after, &steps), "success");
    }

    #[test]
    fn overall_status_is_failed_when_requested_service_start_is_skipped() {
        let params = InstallLocalProvisionDependenciesParams {
            install_docker: true,
            install_postgres: false,
            start_services: true,
            use_sudo: true,
        };
        let after = status(true, None, false, None);
        let steps = vec![LocalProvisionDependencyInstallStep {
            key: "docker_service_start".to_string(),
            status: "skipped".to_string(),
            description: "systemctl unavailable".to_string(),
            command: None,
            detail: None,
            output: None,
        }];

        assert_eq!(compute_overall_status(&params, &after, &steps), "failed");
    }
}
