use crate::unit_client::UnitClientError;
use crate::unitd_docker::UnitdContainer;
use serde::ser::SerializeMap;
use serde::{Serialize, Serializer};
use std::error::Error as StdError;
use std::path::{Path, PathBuf};
use std::{fmt, io};
use which::which;

use crate::runtime_flags::RuntimeFlags;
use crate::unitd_configure_options::UnitdConfigureOptions;
use crate::unitd_process::UnitdProcess;

pub const UNITD_PATH_ENV_KEY: &str = "UNITD_PATH";
pub const UNITD_BINARY_NAMES: [&str; 2] = ["unitd", "unitd-debug"];

#[derive(Debug)]
pub struct UnitdInstance {
    pub process: UnitdProcess,
    pub configure_options: Option<UnitdConfigureOptions>,
    pub errors: Vec<UnitClientError>,
}

impl Serialize for UnitdInstance {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        // 11 = fields to serialize
        let mut state = serializer.serialize_map(Some(11))?;
        let runtime_flags = self
            .process
            .cmd()
            .and_then(|cmd| cmd.flags)
            .map(|flags| flags.to_string());

        let configure_flags = self.configure_options.as_ref().map(|opts| opts.all_flags.clone());

        state.serialize_entry("process", &self.process)?;
        state.serialize_entry("version", &self.version())?;
        state.serialize_entry("control_socket", &self.control_api_socket_address())?;
        state.serialize_entry("log_path", &self.log_path())?;
        state.serialize_entry("pid_path", &self.pid_path())?;
        state.serialize_entry("modules_directory", &self.modules_directory())?;
        state.serialize_entry("state_directory", &self.state_directory())?;
        state.serialize_entry("tmp_directory", &self.tmp_directory())?;
        state.serialize_entry("runtime_flags", &runtime_flags)?;
        state.serialize_entry("configure_flags", &configure_flags)?;
        let string_errors = &self.errors.iter().map(|e| e.to_string()).collect::<Vec<String>>();
        state.serialize_entry("errors", string_errors)?;

        state.end()
    }
}

impl UnitdInstance {
    pub async fn running_unitd_instances() -> Vec<UnitdInstance> {
        Self::collect_unitd_processes(
            UnitdProcess::find_unitd_processes()
                .into_iter()
                .chain(
                    UnitdContainer::find_unitd_containers()
                        .await
                        .into_iter()
                        .map(|x| UnitdProcess::from(&x))
                        .collect::<Vec<_>>(),
                )
                .collect(),
        )
    }

    /// Find all running unitd processes and convert them into UnitdInstances and filter
    /// out all errors by printing them to stderr and leaving errored instances out of
    /// the returned vector.
    fn collect_unitd_processes(processes: Vec<UnitdProcess>) -> Vec<UnitdInstance> {
        Self::map_processes_to_instances(processes).into_iter().collect()
    }

    fn map_processes_to_instances(processes: Vec<UnitdProcess>) -> Vec<UnitdInstance> {
        fn unitd_path_from_process(process: &UnitdProcess) -> Result<Box<Path>, UnitClientError> {
            match process.executable_path() {
                Some(executable_path) => {
                    let is_absolute_working_dir = process
                        .working_dir
                        .as_ref()
                        .map(|p| p.is_absolute())
                        .unwrap_or_default();
                    if executable_path.is_absolute() {
                        Ok(executable_path.to_owned())
                    } else if executable_path.is_relative() && is_absolute_working_dir {
                        let new_path = process
                            .working_dir
                            .as_ref()
                            .unwrap()
                            .join(executable_path)
                            .canonicalize()
                            .map(|path| path.into_boxed_path())
                            .map_err(|error| UnitClientError::UnitdProcessParseError {
                                message: format!("Error canonicalizing unitd executable path: {}", error),
                                pid: process.process_id,
                            })?;
                        Ok(new_path)
                    } else if process.container.is_none() {
                        Err(UnitClientError::UnitdProcessParseError {
                            message: "Unable to get absolute unitd executable path from process".to_string(),
                            pid: process.process_id,
                        })
                    } else {
                        // container case
                        Ok(PathBuf::from("/").into_boxed_path())
                    }
                }
                None => Err(UnitClientError::UnitdProcessParseError {
                    message: "Unable to get unitd executable path from process".to_string(),
                    pid: process.process_id,
                }),
            }
        }

        fn map_process_to_unitd_instance(process: &UnitdProcess) -> UnitdInstance {
            match unitd_path_from_process(process) {
                Ok(_) if process.container.is_some() => {
                    let mut err = vec![];
                    // double check that it is running
                    let running = process.container.as_ref().unwrap().container_is_running();

                    if running.is_none() || !running.unwrap() {
                        err.push(UnitClientError::UnitdProcessParseError {
                            message: "process container is not running".to_string(),
                            pid: process.process_id,
                        });
                    }

                    UnitdInstance {
                        process: process.to_owned(),
                        configure_options: None,
                        errors: err,
                    }
                }
                Ok(unitd_path) => match UnitdConfigureOptions::new(&unitd_path.clone().into_path_buf()) {
                    Ok(configure_options) => UnitdInstance {
                        process: process.to_owned(),
                        configure_options: Some(configure_options),
                        errors: vec![],
                    },
                    Err(error) => {
                        let error = UnitClientError::UnitdProcessExecError {
                            source: error,
                            executable_path: unitd_path.to_string_lossy().parse().unwrap_or_default(),
                            message: "Error running unitd binary to get configure options".to_string(),
                            pid: process.process_id,
                        };
                        UnitdInstance {
                            process: process.to_owned(),
                            configure_options: None,
                            errors: vec![error],
                        }
                    }
                },
                Err(err) => UnitdInstance {
                    process: process.to_owned(),
                    configure_options: None,
                    errors: vec![err],
                },
            }
        }

        processes
            .iter()
            // This converts processes into a UnitdInstance
            .map(map_process_to_unitd_instance)
            .collect()
    }

    fn version(&self) -> Option<String> {
        match self.process.cmd()?.version {
            Some(version) => Some(version),
            None => self.configure_options.as_ref().map(|opts| opts.version.to_string()),
        }
    }

    fn flag_or_default_option<R>(
        &self,
        read_flag: fn(RuntimeFlags) -> Option<R>,
        read_opts: fn(UnitdConfigureOptions) -> Option<R>,
    ) -> Option<R> {
        self.process
            .cmd()?
            .flags
            .and_then(read_flag)
            .or_else(|| self.configure_options.to_owned().and_then(read_opts))
    }

    pub fn control_api_socket_address(&self) -> Option<String> {
        self.flag_or_default_option(
            |flags| flags.control_api_socket_address(),
            |opts| opts.default_control_api_socket_address(),
        )
    }

    pub fn pid_path(&self) -> Option<Box<Path>> {
        self.flag_or_default_option(|flags| flags.pid_path(), |opts| opts.default_pid_path())
    }

    pub fn log_path(&self) -> Option<Box<Path>> {
        self.flag_or_default_option(|flags| flags.log_path(), |opts| opts.default_log_path())
    }

    pub fn modules_directory(&self) -> Option<Box<Path>> {
        self.flag_or_default_option(
            |flags| flags.modules_directory(),
            |opts| opts.default_modules_directory(),
        )
    }

    pub fn state_directory(&self) -> Option<Box<Path>> {
        self.flag_or_default_option(|flags| flags.state_directory(), |opts| opts.default_state_directory())
    }

    pub fn tmp_directory(&self) -> Option<Box<Path>> {
        self.flag_or_default_option(|flags| flags.tmp_directory(), |opts| opts.default_tmp_directory())
    }
}

impl fmt::Display for UnitdInstance {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        const UNKNOWN: &str = "[unknown]";
        let version = self.version().unwrap_or_else(|| String::from("[unknown]"));
        let runtime_flags = self
            .process
            .cmd()
            .and_then(|cmd| cmd.flags)
            .map(|flags| flags.to_string())
            .unwrap_or_else(|| UNKNOWN.into());
        let configure_flags = self
            .configure_options
            .as_ref()
            .map(|opts| opts.all_flags.clone())
            .unwrap_or_else(|| UNKNOWN.into());
        let unitd_path: String = self
            .process
            .executable_path()
            .map(|p| p.to_string_lossy().into())
            .unwrap_or_else(|| UNKNOWN.into());
        let working_dir: String = self
            .process
            .working_dir
            .as_ref()
            .map(|p| p.to_string_lossy().into())
            .unwrap_or_else(|| UNKNOWN.into());
        let socket_address = self.control_api_socket_address().unwrap_or_else(|| UNKNOWN.to_string());
        let child_pids = self
            .process
            .child_pids
            .iter()
            .map(u64::to_string)
            .collect::<Vec<String>>()
            .join(", ");

        writeln!(
            f,
            "{} instance [pid: {}, version: {}]:",
            self.process.binary_name, self.process.process_id, version
        )?;
        writeln!(f, "  Executable: {}", unitd_path)?;
        writeln!(f, "  Process working directory: {}", working_dir)?;
        write!(f, "  Process ownership: ")?;
        if let Some(user) = &self.process.user {
            writeln!(f, "name: {}, uid: {}, gid: {}", user.name, user.uid, user.gid)?;
        } else {
            writeln!(f, "{}", UNKNOWN)?;
        }
        write!(f, "  Process effective ownership: ")?;
        if let Some(user) = &self.process.effective_user {
            writeln!(f, "name: {}, uid: {}, gid: {}", user.name, user.uid, user.gid)?;
        } else {
            writeln!(f, "{}", UNKNOWN)?;
        }

        writeln!(f, "  API control unix socket: {}", socket_address)?;
        writeln!(f, "  Child processes ids: {}", child_pids)?;
        writeln!(f, "  Runtime flags: {}", runtime_flags)?;
        writeln!(f, "  Configure options: {}", configure_flags)?;

        if let Some(ctr) = &self.process.container {
            writeln!(f, "  Container:")?;
            writeln!(f, "    Platform: {}", ctr.platform)?;
            if let Some(id) = ctr.container_id.clone() {
                writeln!(f, "    Container ID: {}", id)?;
            }
            writeln!(f, "    Mounts:")?;
            for (k, v) in &ctr.mounts {
                writeln!(f, "      {} => {}", k.to_string_lossy(), v.to_string_lossy())?;
            }
        }

        if !self.errors.is_empty() {
            write!(f, "  Errors:")?;
            for error in &self.errors {
                write!(f, "\n    {}", error)?;
            }
        }

        Ok(())
    }
}

pub fn find_executable_path(specific_path: Result<String, Box<dyn StdError>>) -> Result<PathBuf, Box<dyn StdError>> {
    fn find_unitd_in_system_path() -> Vec<PathBuf> {
        UNITD_BINARY_NAMES
            .iter()
            .map(which)
            .filter_map(Result::ok)
            .collect::<Vec<PathBuf>>()
    }

    match specific_path {
        Ok(path) => Ok(PathBuf::from(path)),
        Err(_) => {
            let unitd_paths = find_unitd_in_system_path();
            if unitd_paths.is_empty() {
                let err_msg = format!(
                    "Could not find unitd in system path or in UNITD_PATH environment variable. Searched for: {:?}",
                    UNITD_BINARY_NAMES
                );
                let err = io::Error::new(io::ErrorKind::NotFound, err_msg);
                Err(Box::from(err))
            } else {
                Ok(unitd_paths[0].clone())
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use rand::rngs::StdRng;
    use rand::{RngCore, SeedableRng};

    // We don't need a secure seed for testing, in fact it is better that we have a
    // predictable value
    const SEED: [u8; 32] = [
        0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29,
        30, 31,
    ];
    #[tokio::test]
    async fn can_find_unitd_instances() {
        UnitdInstance::running_unitd_instances().await.iter().for_each(|p| {
            println!("{:?}", p);
            println!("Runtime Flags: {:?}", p.process.cmd().map(|c| c.flags));
            println!("Temp directory: {:?}", p.tmp_directory());
        })
    }

    fn mock_process<S: Into<String>>(
        rng: &mut StdRng,
        binary_name: S,
        executable_path: Option<String>,
    ) -> UnitdProcess {
        UnitdProcess {
            process_id: rng.next_u32() as u64,
            binary_name: binary_name.into(),
            executable_path: executable_path.map(|p| Box::from(Path::new(&p))),
            environ: vec![],
            all_cmds: vec![],
            working_dir: Some(Box::from(Path::new("/opt/unit"))),
            child_pids: vec![],
            user: None,
            effective_user: None,
            container: None,
        }
    }

    #[test]
    fn will_list_without_errors_valid_processes() {
        let specific_path = std::env::var(UNITD_PATH_ENV_KEY).map_err(|error| Box::new(error) as Box<dyn StdError>);
        let binding = match find_executable_path(specific_path) {
            Ok(path) => path,
            Err(error) => {
                eprintln!("Could not find unitd executable path: {} - skipping test", error);
                return;
            }
        };
        let binary_name = binding
            .file_name()
            .expect("Could not get binary name")
            .to_string_lossy()
            .to_string();
        let unitd_path = binding.to_string_lossy();
        let mut rng: StdRng = SeedableRng::from_seed(SEED);

        let processes = vec![
            mock_process(&mut rng, &binary_name, Some(unitd_path.to_string())),
            mock_process(&mut rng, &binary_name, Some(unitd_path.to_string())),
        ];
        let instances = UnitdInstance::collect_unitd_processes(processes);
        // assert_eq!(instances.len(), 3);
        instances.iter().for_each(|p| {
            assert_eq!(p.errors.len(), 0, "Expected no errors, got: {:?}", p.errors);
        })
    }
}
