use rayon::prelude::*;
use std::{
    collections::HashSet,
    env,
    fs::{self, read_to_string, File},
    io::{BufRead, BufReader, Read},
    path::Path,
    process::{Command, Stdio},
    sync::{Arc, Mutex},
    time::Duration,
};

#[cfg(target_os = "linux")]
use {once_cell::sync::Lazy, regex::Regex};

pub(crate) fn help() {
    println!(
        r#"Usage: rsftch [-h / --help / --usage] [-v / --version] [-o / --override <distro name>] [-m / --margin <margin>] [--ignore-config] [--config <absolute path to config>]
        
      -h, --help, --usage         Bring up this menu.
      -v, --version               Print version number.
      -o, --override              Overrides distribution, affects ASCII and "distro" info. Running without an argument prints all possible options.
      -m, --margin                Add margin to the info sections, default 1. E.g. `rsftch --info distro` would output: "EndeavourOS".
          --config                Specify another info config file to be used.
          --ignore-config         Ignores configuration and uses the example one.

    Configuration file is located at: ~/.config/rsftch/info.json"#
    );
}

pub(crate) fn whoami() -> String {
    let output = Command::new("whoami")
        .output()
        .expect("`whoami` failed, are you on a Unix-like operating system?");
    String::from_utf8_lossy(&output.stdout).trim().to_string()
}

pub(crate) fn timezone() -> String {
    let timezone_path = Path::new("/etc/timezone");
    if timezone_path.exists() {
        if let Ok(timezone) = fs::read_to_string(timezone_path) {
            return timezone.trim().to_string();
        }
    }

    let localtime_path = Path::new("/etc/localtime");
    if localtime_path.exists() {
        if let Ok(symlink_target) = fs::read_link(localtime_path) {
            if let Some(target_str) = symlink_target.to_str() {
                if target_str.contains("/zoneinfo/") {
                    if let Some(tz) = target_str.split("/zoneinfo/").last() {
                        return tz.to_string();
                    }
                }
            }
        }
    }

    String::new()
}

pub(crate) fn cpu_temp() -> String {
    #[cfg(target_os = "linux")]
    {
        static REGEX: Lazy<Regex> =
            Lazy::new(|| Regex::new(r"Package id 0:\s+\+(\d+\.\d+)°C").unwrap());

        let output = Command::new("sensors").output().unwrap();
        let output_str = String::from_utf8_lossy(&output.stdout);

        REGEX
            .captures(&output_str)
            .map_or_else(|| "(N/A)".to_string(), |caps| format!("({}°C)", &caps[1]))
    }

    #[cfg(target_os = "netbsd")]
    {
        Command::new("envstat")
            .output()
            .ok()
            .and_then(|output| String::from_utf8(output.stdout).ok())
            .and_then(|output_str| {
                output_str
                    .lines()
                    .skip_while(|line| *line != "[acpitz0]")
                    .nth(1)
                    .and_then(|line| line.split(':').nth(1))
                    .map(|s| s.split_whitespace().next().unwrap_or(""))
                    .and_then(|temp_str| temp_str.parse::<f64>().ok())
                    .map(|temp| format!("({:.1}°C)", temp))
            })
            .unwrap_or(String::from("(N/A)"))
    }
}

fn gpu_temp() -> String {
    #[cfg(target_os = "linux")]
    {
        Command::new("nvidia-smi")
            .arg("--query-gpu=temperature.gpu")
            .arg("--format=csv,noheader")
            .output()
            .ok()
            .and_then(|output| String::from_utf8(output.stdout).ok())
            .and_then(|temp_str| {
                temp_str
                    .lines()
                    .next()
                    .and_then(|s| s.trim().parse::<f64>().ok())
            })
            .map(|temp| format!("({:.1}°C)", temp))
            .unwrap_or_else(|| {
                Command::new("sensors")
                    .output()
                    .ok()
                    .and_then(|output| String::from_utf8(output.stdout).ok())
                    .and_then(|output_str| {
                        output_str
                            .lines()
                            .find(|line| {
                                line.contains("temp1:")
                                    || line.contains("edge:")
                                    || line.contains("junction:")
                                    || line.contains("mem:")
                            })
                            .and_then(|line| line.split_whitespace().nth(1))
                            .and_then(|temp_str| {
                                temp_str.trim_end_matches("°C").parse::<f64>().ok()
                            })
                            .map(|temp| format!("({:.1}°C)", temp))
                    })
                    .unwrap_or(String::from("(N/A)"))
            })
    }

    #[cfg(target_os = "netbsd")]
    {
        Command::new("envstat")
            .output()
            .ok()
            .and_then(|output| String::from_utf8(output.stdout).ok())
            .and_then(|output_str| {
                output_str
                    .lines()
                    .skip_while(|line| *line != "[acpitz2]")
                    .nth(1)
                    .and_then(|line| line.split(':').nth(1))
                    .map(|s| s.split_whitespace().next().unwrap_or(""))
                    .and_then(|temp_str| temp_str.parse::<f64>().ok())
                    .map(|temp| format!("({:.1}°C)", temp))
            })
            .unwrap_or(String::from("(N/A)"))
    }
}

fn extract_gpu_name(line: &str) -> String {
    let parts: Vec<&str> = line.split('[').collect();
    if parts.len() >= 3 {
        return parts[2].split(']').next().unwrap_or("Unknown GPU").trim().to_string();
    }
    String::from("Unknown GPU")
}

pub(crate) fn gpu_info() -> String {
    #[cfg(target_os = "linux")]
    {
        let output = Command::new("lspci")
            .arg("-nnk")
            .output()
            .map(|output| String::from_utf8_lossy(&output.stdout).to_string())
            .unwrap_or_else(|_| format!("N/A {}", gpu_temp()));

        let reader = BufReader::new(output.as_bytes());

        for line in reader.lines().flatten() {
            if line.contains("NVIDIA") {
                let prefix = "NVIDIA";
                let gpu_name = extract_gpu_name(&line);
                return format!("{} {} {}", prefix, gpu_name, gpu_temp());
            } else if line.contains("AMD") {
                let prefix = if line.contains("Radeon") {
                    "AMD"
                } else {
                    "AMD Radeon"
                };
                let gpu_name = extract_gpu_name(&line);
                return format!("{} {} {}", prefix, gpu_name, gpu_temp());
            } else if line.contains("Intel") {
                if line.contains("VGA compatible controller") || line.contains("3D controller") {
                    let prefix = "Intel Integrated";
                    let gpu_name = extract_gpu_name(&line);
                    return format!("{} {} {}", prefix, gpu_name, gpu_temp());
                }
            }
        }

        format!("N/A {}", gpu_temp())
    }
    #[cfg(target_os = "netbsd")]
    {
        let formatted_str = Command::new("pcictl")
            .args(&["pci0", "list"])
            .output()
            .map(|output| String::from_utf8_lossy(&output.stdout).to_string())
            .unwrap_or_else(|_| format!("N/A {}", gpu_temp()))
            .lines()
            .find(|&l| l.contains("VGA display"))
            .and_then(|l| l.rsplitn(2, ':').next())
            .map(|name| {
                name.trim()
                    .split_at(name.find('(').unwrap_or(0))
                    .0
                    .trim()
                    .to_string()
            })
            .unwrap_or_else(|| format!("N/A {}", gpu_info()));

        format!("{} {}", formatted_str, gpu_temp())
    }
}

pub(crate) fn disk_usage() -> String {
    let output_str = match Command::new("df").arg("-h").output() {
        Ok(output) if output.status.success() => {
            String::from_utf8(output.stdout).unwrap_or_default()
        }
        _ => return String::from("N/A"),
    };

    let line = output_str.lines().find(|line| line.starts_with('/'));

    if let Some(line) = line {
        let parts: Vec<_> = line.split_whitespace().collect();
        if parts.len() >= 5 {
            let filesystem = parts[0];
            let used = parts[2];
            let size = parts[1];
            let capacity = parts[4];
            return format!("({}) {} / {} ({})", filesystem, used, size, capacity);
        }
    }

    String::new()
}

pub(crate) fn cpu_info() -> String {
    let cpuinfo_file = match read_to_string("/proc/cpuinfo") {
        Ok(content) => content,
        Err(_) => return format!("N/A {}", cpu_temp()),
    };

    let keys: HashSet<&str> = [
        "model name",
        "Hardware",
        "Processor",
        "^cpu model",
        "chip type",
        "^cpu type",
    ]
    .iter()
    .cloned()
    .collect();

    for line in cpuinfo_file.lines() {
        if let Some(pos) = line.find(": ") {
            let key = &line[..pos];
            let value = &line[pos + 2..].trim();

            if keys.contains(&key.trim()) {
                return format!("{}{}", value.split('@').next().unwrap_or(value), cpu_temp());
            }
        }
    }

    format!("N/A {}", cpu_temp())
}

fn package_managers() -> Vec<String> {
    let possible_managers = vec![
        "xbps-query",
        "dnf",
        "rpm",
        "apt",
        "pacman",
        "emerge",
        "yum",
        "zypper",
        "apk",
        "pkg_info",
        "pkg",
    ];

    possible_managers
        .par_iter()
        .filter_map(|manager| {
            let version_command = match *manager {
                "pkg_info" => "-V",
                "emerge" => "--help",
                _ => "--version",
            };

            if Command::new(manager)
                .arg(version_command)
                .output()
                .ok()
                .map_or(false, |result| result.status.success())
            {
                Some(manager.to_string())
            } else {
                None
            }
        })
        .collect()
}

fn count_packages(command: &str, args: &[&str]) -> Option<i16> {
    let mut cmd = Command::new(command)
        .args(args)
        .stdout(Stdio::piped())
        .spawn()
        .ok()?;

    let output = cmd.stdout.take()?;
    let reader = BufReader::new(output);
    let line_count = reader.lines().count() as i16;

    let _ = cmd.wait().ok()?;
    Some(line_count)
}

pub(crate) fn packages() -> String {
    let managers = package_managers();
    let packs_numbers = Arc::new(Mutex::new(Vec::new()));

    managers.par_iter().for_each(|manager| {
        let count = match manager.as_str() {
            "xbps-query" => count_packages(manager, &["-l"]),
            "dnf" | "yum" => count_packages(manager, &["list", "installed"]),
            "rpm" => count_packages(manager, &["-qa", "--last"]),
            "apt" => count_packages("dpkg", &["--list"]),
            "pacman" => count_packages(manager, &["-Q"]),
            "zypper" => count_packages(manager, &["se"]),
            "apk" => count_packages(manager, &["list", "--installed"]),
            "pkg_info" => count_packages("ls", &["/usr/pkg/pkgdb/"]).map(|x| x - 1),
            "pkg" => count_packages(manager, &["info"]),
            "emerge" => {
                if os_pretty_name(None, "ID")
                    .unwrap_or_default()
                    .to_ascii_lowercase()
                    .contains("funtoo")
                {
                    count_packages("find", &["/var/db/pkg/", "-name", "PF"])
                } else {
                    count_packages(manager, &["-I"])
                }
            }
            _ => None,
        };

        if let Some(count) = count {
            packs_numbers.lock().unwrap().push(count);
        }
    });

    let summed: i16 = packs_numbers.lock().unwrap().par_iter().sum();

    match managers.is_empty() {
        false => format!("{} ({})", summed, managers.join(", ")),
        true => String::from("N/A"),
    }
}

pub(crate) fn res() -> String {
    let output = match Command::new("xrandr").arg("--query").output() {
        Ok(out) => out,
        Err(_) => return String::from("N/A"),
    };

    String::from_utf8_lossy(&output.stdout)
        .lines()
        .filter_map(|line| {
            if let Some(index) = line.find(" connected") {
                let line = &line[index + 1..];
                if let Some(resolution) = line.split_whitespace().find(|s| s.contains('x')) {
                    Some(resolution.split('+').next().unwrap_or("").to_string())
                } else {
                    None
                }
            } else {
                None
            }
        })
        .collect::<Vec<String>>()
        .join(", ")
}

pub(crate) fn uptime() -> String {
    let mut line = String::new();

    File::open("/proc/uptime")
        .map_err(|_| "NA".to_string())
        .and_then(|file| {
            BufReader::new(file)
                .read_line(&mut line)
                .map_err(|_| "N/A".to_string())
        })
        .ok();

    let uptime_secs: f64 = line
        .split_whitespace()
        .next()
        .and_then(|val| val.parse().ok())
        .unwrap_or_default();

    format_duration(Duration::from_secs_f64(uptime_secs)).to_string()
}

fn format_duration(duration: Duration) -> String {
    let seconds = duration.as_secs();
    let mut values = vec![
        (seconds / (24 * 3600), "days"),
        ((seconds % (24 * 3600)) / 3600, "hours"),
        ((seconds % 3600) / 60, "minutes"),
        (seconds % 60, "seconds"),
    ];

    values.retain(|&(value, _)| value > 0);

    values
        .iter()
        .map(|&(value, unit)| format!("{} {}", value, unit))
        .collect::<Vec<_>>()
        .join(", ")
}

fn search_file(custom_paths: Vec<&'static str>, search_variable: &str) -> Option<String> {
    for path in custom_paths.iter() {
        if let Ok(content) = fs::read_to_string(path) {
            for line in content.lines() {
                if line.starts_with(search_variable) {
                    if let Some(name) = line.split('=').nth(1) {
                        return Some(name.trim_matches('"').to_string());
                    }
                }
            }
        }
    }
    None
}

pub(crate) fn os_pretty_name(ascii_override: Option<String>, identifier: &str) -> Option<String> {
    if ascii_override.is_some() {
        return ascii_override;
    }

    search_file(vec!["/etc/os-release", "/etc/lsb-release"], identifier)
}

pub(crate) fn wm() -> String {
    if env::var("DISPLAY").is_err() {
        return String::new();
    }

    for env_var in &[
        "XDG_SESSION_DESKTOP",
        "XDG_CURRENT_DESKTOP",
        "DESKTOP_SESSION",
    ] {
        if let Ok(de) = env::var(env_var) {
            return de;
        }
    }

    let path = format!("{}/.xinitrc", env::var("HOME").unwrap_or_default());
    if let Ok(mut file) = File::open(&path) {
        let mut buf = String::new();
        if file.read_to_string(&mut buf).is_ok() {
            if let Some(last_line) = buf.lines().last() {
                let last_word = last_line.split(' ').last().unwrap_or("");
                return last_word.to_string();
            }
        }
    }

    String::from("N/A")
}

fn parse_memory_value(line: &str) -> u64 {
    line.split_whitespace()
        .nth(1)
        .unwrap_or("0")
        .parse::<u64>()
        .unwrap_or(0)
}

pub(crate) fn mem() -> String {
    let kb_to_gb = |kilobytes: u64| kilobytes as f64 / (1024.0 * 1024.0);

    if let Ok(file) = File::open("/proc/meminfo") {
        let reader = BufReader::new(file);
        let mut mem_total: u64 = 0;

        #[cfg(target_os = "linux")]
        {
            let mut mem_available: u64 = 0;
            for line in reader.lines() {
                if let Ok(line) = line {
                    if line.starts_with("MemTotal:") {
                        mem_total = parse_memory_value(&line);
                    } else if line.starts_with("MemAvailable:") {
                        mem_available = parse_memory_value(&line);
                    }
                }
            }

            let used = mem_total - mem_available;
            return format!("{:.2} GiB / {:.2} GiB", kb_to_gb(used), kb_to_gb(mem_total));
        }

        #[cfg(target_os = "netbsd")]
        {
            let mut mem_free: u64 = 0;
            for line in reader.lines() {
                if let Ok(line) = line {
                    if line.starts_with("MemTotal:") {
                        mem_total = parse_memory_value(&line);
                    } else if line.starts_with("MemAvailable:") || line.starts_with("MemFree:") {
                        mem_free = parse_memory_value(&line);
                    }
                }
            }

            let used = mem_total - mem_free;
            return format!("{:.2} GiB / {:.2} GiB", kb_to_gb(used), kb_to_gb(mem_total));
        }
    }

    String::from("N/A")
}

pub(crate) fn uname(arg: &str, ascii_override: Option<String>) -> String {
    if ascii_override.is_some() {
        return ascii_override.unwrap();
    }

    let output = Command::new("uname")
        .arg(arg)
        .output()
        .expect(format!("`uname` failed {arg}, this should not happen.").as_str());
    String::from_utf8_lossy(&output.stdout).trim().to_string()
}

pub(crate) fn shell_name() -> String {
    let shell = env::var("SHELL").expect("SHELL not set");
    let parts: Vec<&str> = shell.split('/').collect();
    parts.last().unwrap().to_string()
}

pub(crate) fn terminal() -> String {
    env::var("TERM").unwrap_or("N/A".to_string())
}
