use crate::unitd_cmd::UnitdCmd;
use crate::unitd_docker::{pid_is_dockerized, UnitdContainer};
use crate::unitd_instance::UNITD_BINARY_NAMES;
use crate::unitd_process_user::UnitdProcessUser;
use serde::ser::SerializeMap;
use serde::{Serialize, Serializer};
use std::collections::HashMap;
use std::path::Path;
use sysinfo::{Pid, Process, ProcessRefreshKind, System, UpdateKind, Users};

#[derive(Debug, Clone)]
pub struct UnitdProcess {
    pub binary_name: String,
    pub process_id: u64,
    pub executable_path: Option<Box<Path>>,
    pub environ: Vec<String>,
    pub all_cmds: Vec<String>,
    pub working_dir: Option<Box<Path>>,
    pub child_pids: Vec<u64>,
    pub user: Option<UnitdProcessUser>,
    pub effective_user: Option<UnitdProcessUser>,
    pub container: Option<UnitdContainer>,
}

impl Serialize for UnitdProcess {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        // 6 = fields to serialize
        let mut state = serializer.serialize_map(Some(6))?;
        state.serialize_entry("pid", &self.process_id)?;
        state.serialize_entry("user", &self.user)?;
        state.serialize_entry("effective_user", &self.effective_user)?;
        state.serialize_entry("executable", &self.executable_path())?;
        state.serialize_entry("child_pids", &self.child_pids)?;
        state.serialize_entry("container", &self.container)?;
        state.end()
    }
}

impl UnitdProcess {
    pub fn find_unitd_processes() -> Vec<UnitdProcess> {
        let process_refresh_kind = ProcessRefreshKind::new()
            .with_cmd(UpdateKind::Always)
            .with_cwd(UpdateKind::Always)
            .with_exe(UpdateKind::Always)
            .with_user(UpdateKind::Always);
        let refresh_kind = sysinfo::RefreshKind::new().with_processes(process_refresh_kind);
        let sys = System::new_with_specifics(refresh_kind);
        let unitd_processes: HashMap<&Pid, &Process> = sys
            .processes()
            .iter()
            .filter(|p| {
                let process_name = p.1.name();
                UNITD_BINARY_NAMES.contains(&process_name)
            })
            .collect::<HashMap<&Pid, &Process>>();
        let users = Users::new_with_refreshed_list();

        unitd_processes
            .iter()
            // Filter out child processes
            .filter(|p| {
                #[cfg(target_os = "linux")]
                if pid_is_dockerized(p.0.as_u32().into()) {
                    return false;
                }
                let parent_pid = p.1.parent();
                match parent_pid {
                    Some(pid) => !unitd_processes.contains_key(&pid),
                    None => false,
                }
            })
            .map(|p| {
                let tuple = p.to_owned();
                /* The sysinfo library only supports 32-bit pids, yet larger values are possible
                 * if the OS is configured to support it, thus we use 64-bit integers internally
                 * because it is just a matter of time until the library changes to larger values. */
                let pid = *tuple.0;
                let process = *tuple.1;
                let process_id: u64 = pid.as_u32().into();
                let executable_path: Option<Box<Path>> = process.exe().map(|p| p.to_path_buf().into_boxed_path());
                let environ: Vec<String> = process.environ().into();
                let cmd: Vec<String> = process.cmd().into();
                let working_dir: Option<Box<Path>> = process.cwd().map(|p| p.to_path_buf().into_boxed_path());
                let child_pids = unitd_processes
                    .iter()
                    .filter_map(|p| p.to_owned().1.parent())
                    .filter(|parent_pid| parent_pid == pid)
                    .map(|p| p.as_u32() as u64)
                    .collect::<Vec<u64>>();

                let user = process
                    .user_id()
                    .and_then(|uid| users.get_user_by_id(uid))
                    .map(UnitdProcessUser::from);
                let effective_user = process
                    .effective_user_id()
                    .and_then(|uid| users.get_user_by_id(uid))
                    .map(UnitdProcessUser::from);

                UnitdProcess {
                    binary_name: process.name().to_string(),
                    process_id,
                    executable_path,
                    environ,
                    all_cmds: cmd,
                    working_dir,
                    child_pids,
                    user,
                    effective_user,
                    container: None,
                }
            })
            .collect::<Vec<UnitdProcess>>()
    }

    pub fn cmd(&self) -> Option<UnitdCmd> {
        if self.all_cmds.is_empty() {
            return None;
        }

        match UnitdCmd::new(self.all_cmds[0].clone(), self.binary_name.as_ref()) {
            Ok(cmd) => Some(cmd),
            Err(error) => {
                eprintln!("Failed to parse process cmd: {}", error);
                None
            }
        }
    }

    pub fn executable_path(&self) -> Option<Box<Path>> {
        if self.executable_path.is_some() {
            return self.executable_path.clone();
        }
        self.cmd().and_then(|cmd| cmd.process_executable_path)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn can_parse_runtime_cmd_absolute_path(binary_name: &str) {
        let cmd = format!(
            "unit: main v1.28.0 [/usr/sbin/{} --log /var/log/unit.log --pid /var/run/unit.pid]",
            binary_name
        );
        let unitd_cmd = UnitdCmd::new(cmd, binary_name).expect("Failed to parse unitd cmd");
        assert_eq!(unitd_cmd.version.unwrap(), "1.28.0");
        assert_eq!(
            unitd_cmd.process_executable_path.unwrap().to_string_lossy(),
            format!("/usr/sbin/{}", binary_name)
        );
        let flags = unitd_cmd.flags.unwrap();
        assert_eq!(flags.get_flag_value("log").unwrap(), "/var/log/unit.log");
        assert_eq!(flags.get_flag_value("pid").unwrap(), "/var/run/unit.pid");
    }

    fn can_parse_runtime_cmd_relative_path(binary_name: &str) {
        let cmd = format!(
            "unit: main v1.29.0 [./sbin/{} --no-daemon --tmp /tmp --something]",
            binary_name
        );
        let unitd_cmd = UnitdCmd::new(cmd, binary_name).expect("Failed to parse unitd cmd");
        assert_eq!(unitd_cmd.version.unwrap(), "1.29.0");
        assert_eq!(
            unitd_cmd.process_executable_path.unwrap().to_string_lossy(),
            format!("./sbin/{}", binary_name)
        );
        let flags = unitd_cmd.flags.unwrap();
        assert_eq!(flags.get_flag_value("tmp").unwrap(), "/tmp");
        assert!(flags.has_flag("something"));
    }

    #[test]
    fn can_parse_runtime_cmd_unitd_absolute_path() {
        can_parse_runtime_cmd_absolute_path("unitd");
    }

    #[test]
    fn can_parse_runtime_cmd_unitd_debug_absolute_path() {
        can_parse_runtime_cmd_absolute_path("unitd-debug");
    }

    #[test]
    fn can_parse_runtime_cmd_unitd_relative_path() {
        can_parse_runtime_cmd_relative_path("unitd");
    }

    #[test]
    fn can_parse_runtime_cmd_unitd_debug_relative_path() {
        can_parse_runtime_cmd_relative_path("unitd-debug");
    }
}
