use crate::verbose::Verbosity;
use crate::DockerCommand;
use colored::*;
use regex::Regex;
use serde_yaml::{to_string, Error, Mapping, Value};
use std::cmp::{max, min};
use std::collections::BTreeMap;
use std::path::Path;
use std::sync::mpsc::{Receiver, Sender};
use std::sync::{mpsc, Arc, Mutex};
use std::{process, thread};
lazy_static! {
    static ref EMPTY_MAP: Mapping = Mapping::default();
    static ref ENV_NAME_REGEX: Regex = Regex::new(r"^\w+$").unwrap();
    static ref QUOTED_NUM_REGEX: Regex = Regex::new(r"^'[0-9]+'$").unwrap();
}
pub struct ComposeYaml {
    map: BTreeMap<String, Value>,
}
#[derive(Clone)]
pub struct RemoteTag {
    pub remote_tag: String,
    pub remote_tag_filter: Option<(Regex, bool)>,
    pub ignore_unauthorized: bool,
    pub verbosity: Verbosity,
    pub remote_progress_verbosity: Verbosity,
    pub threads: u8,
}
impl ComposeYaml {
    pub fn new(yaml: &str) -> Result<ComposeYaml, Error> {
        let map = serde_yaml::from_str(yaml)?;
        Ok(ComposeYaml { map })
    }
    pub fn to_string(&self) -> Result<String, Error> {
        let yaml_string = to_string(&self.map)?;
        Ok(yaml_string)
    }
    pub fn get_root_element(&self, element_name: &str) -> Option<&Mapping> {
        let value = self.map.get(element_name);
        value.map(|v| v.as_mapping()).unwrap_or_default()
    }
    pub fn get_root_element_names(&self, element_name: &str) -> Vec<&str> {
        let elements = self.get_root_element(element_name).unwrap_or(&EMPTY_MAP);
        elements
            .keys()
            .map(|k| k.as_str().unwrap())
            .collect::<Vec<_>>()
    }
    pub fn get_services(&self) -> Option<&Mapping> {
        self.get_root_element("services")
    }
    pub fn get_profiles_names(&self) -> Option<Vec<&str>> {
        let services = self.get_services()?;
        let mut profiles = services
            .values()
            .flat_map(|v| v.as_mapping())
            .flat_map(|s| s.get("profiles"))
            .flat_map(|p| p.as_sequence())
            .flat_map(|seq| seq.iter())
            .flat_map(|e| e.as_str())
            .collect::<Vec<_>>();
        profiles.sort();
        profiles.dedup();
        Some(profiles)
    }
    pub fn get_images(
        &self,
        filter_by_tag: Option<&str>,
        remote_tag: Option<&RemoteTag>,
    ) -> Option<Vec<String>> {
        let services = self.get_services()?;
        let mut images = services
            .values()
            .flat_map(|v| v.as_mapping())
            .flat_map(|s| s.get("image"))
            .flat_map(|p| p.as_str())
            .filter(|image| match filter_by_tag {
                None => true,
                Some(tag) => {
                    let image_parts = image.split(':').collect::<Vec<_>>();
                    let image_tag = if image_parts.len() > 1 {
                        *image_parts.get(1).unwrap()
                    } else {
                        "latest"
                    };
                    tag == image_tag
                }
            })
            .collect::<Vec<_>>();
        images.sort();
        images.dedup();
        if let Some(remote) = remote_tag {
            let show_remote_progress = matches!(remote.verbosity, Verbosity::Verbose)
                || matches!(remote.remote_progress_verbosity, Verbosity::Verbose);
            let input = Arc::new(Mutex::new(
                images
                    .iter()
                    .rev()
                    .map(|e| e.to_string())
                    .collect::<Vec<String>>(),
            ));
            let remote_arc = Arc::new(remote.clone());
            let mut updated_images: Vec<String> = Vec::with_capacity(images.len());
            let (tx, rx): (Sender<String>, Receiver<String>) = mpsc::channel();
            let mut thread_children = Vec::new();
            let nthreads = max(1, min(images.len(), remote.threads as usize));
            if matches!(remote.verbosity, Verbosity::Verbose) {
                eprintln!(
                    "{}: spawning {} threads to fetch remote info from {} images",
                    "DEBUG".green(),
                    nthreads,
                    images.len()
                )
            }
            for _ in 0..nthreads {
                let input = Arc::clone(&input);
                let remote = Arc::clone(&remote_arc);
                let thread_tx = tx.clone();
                let child = thread::spawn(move || {
                    loop {
                        let mut v = input.lock().unwrap();
                        let last = v.pop(); drop(v); if let Some(image) = last {
                            let image_parts = image.split(':').collect::<Vec<_>>();
                            let image_name = *image_parts.first().unwrap();
                            let remote_image = format!("{}:{}", image_name, remote.remote_tag);
                            if remote
                                .remote_tag_filter
                                .as_ref()
                                .map(|r| (r.1, r.0.is_match(&image)))
                                .map(|(affirmative_expr, is_match)| {
                                    (affirmative_expr && is_match)
                                        || (!affirmative_expr && !is_match)
                                })
                                .unwrap_or(true)
                            {
                                match Self::has_manifest(
                                    &remote,
                                    &remote_image,
                                    show_remote_progress,
                                ) {
                                    true => thread_tx.send(remote_image).unwrap(),
                                    false => thread_tx.send(image).unwrap(),
                                }
                            } else {
                                if show_remote_progress {
                                    eprintln!(
                                        "{}: remote manifest for image {} ... {} ",
                                        "DEBUG".green(),
                                        image_name.yellow(),
                                        "skipped".bright_black()
                                    );
                                }
                                thread_tx.send(image.to_string()).unwrap();
                            }
                        } else {
                            break; }
                    }
                });
                thread_children.push(child);
            }
            for _ in 0..images.len() {
                let out = rx.recv().unwrap();
                updated_images.push(out);
            }
            for child in thread_children {
                child.join().unwrap_or_else(|e| {
                    eprintln!(
                        "{}: child thread panicked while fetching remote images info: {:?}",
                        "ERROR".red(),
                        e
                    );
                });
            }
            updated_images.sort();
            return Some(updated_images);
        }
        Some(images.iter().map(|i| i.to_string()).collect::<Vec<_>>())
    }
    fn has_manifest(remote: &RemoteTag, remote_image: &str, show_remote_progress: bool) -> bool {
        let command = DockerCommand::new(remote.verbosity.clone());
        let inspect_output = command
            .get_manifest_inspect(remote_image)
            .unwrap_or_else(|e| {
                eprintln!(
                    "{}: fetching image manifest for {}: {}",
                    "ERROR".red(),
                    remote_image,
                    e
                );
                process::exit(151);
            });
        if inspect_output.status.success() {
            if show_remote_progress {
                eprintln!(
                    "{}: remote manifest for image {} ... {} ",
                    "DEBUG".green(),
                    remote_image.yellow(),
                    "found".green()
                );
            }
            true
        } else {
            let exit_code = command.exit_code(&inspect_output);
            let stderr = String::from_utf8(inspect_output.stderr).unwrap();
            if stderr.contains("no such manifest")
                || (remote.ignore_unauthorized && stderr.contains("unauthorized:"))
            {
                if show_remote_progress {
                    eprintln!(
                        "{}: remote manifest for image {} ... {} ",
                        "DEBUG".green(),
                        remote_image.yellow(),
                        "not found".purple()
                    );
                }
                false
            } else {
                eprintln!(
                    "{}: fetching image manifest for {}: {}",
                    "ERROR".red(),
                    remote_image,
                    stderr
                );
                process::exit(exit_code);
            }
        }
    }
    pub fn update_images_with_remote_tag(&mut self, remote_tag: &RemoteTag) {
        if let Some(images_with_remote) = self.get_images(None, Some(remote_tag)) {
            let services_names = self
                .get_root_element_names("services")
                .iter()
                .map(|s| s.to_string())
                .collect::<Vec<_>>();
            let services_op = self
                .map
                .get_mut("services")
                .and_then(|v| v.as_mapping_mut());
            if let Some(services) = services_op {
                for service_name in services_names {
                    let service = services.entry(Value::String(service_name.to_string()));
                    service.and_modify(|serv| {
                        if let Some(image_value) = serv.get_mut("image") {
                            let image = image_value
                                .as_str()
                                .map(|i| i.to_string())
                                .unwrap_or_default();
                            let image_name = image.split(':').next().unwrap_or_default();
                            let remote_image_op = images_with_remote.iter().find(|i| {
                                let remote_image_name = i.split(':').next().unwrap_or_default();
                                image_name == remote_image_name
                            });
                            if let Some(remote_image) = remote_image_op {
                                if remote_image != &image {
                                    if let Value::String(string) = image_value {
                                        string.replace_range(.., remote_image);
                                    }
                                }
                            }
                        }
                    });
                }
            }
        }
    }
    pub fn get_service(&self, service_name: &str) -> Option<&Mapping> {
        let services = self.get_services()?;
        let service = services.get(service_name);
        service.map(|v| v.as_mapping()).unwrap_or_default()
    }
    pub fn get_service_envs(&self, service: &Mapping) -> Option<Vec<String>> {
        let envs = service.get("environment")?;
        match envs.as_sequence() {
            Some(seq) => Some(
                seq.iter()
                    .map(|v| {
                        let val = v.as_str().unwrap_or("");
                        if ENV_NAME_REGEX.captures(val).is_some() {
                            format!("{val}=")
                        } else {
                            String::from(val)
                        }
                    })
                    .collect::<Vec<_>>(),
            ),
            None => Some(
                envs.as_mapping()
                    .unwrap_or(&EMPTY_MAP)
                    .into_iter()
                    .map(|(k, v)| {
                        let env = k.as_str().unwrap_or("".as_ref());
                        let val = to_string(v).unwrap_or("".to_string());
                        let val = val.trim_end();
                        if val.contains(' ') {
                            if val.contains('"') {
                                format!("{env}='{val}'")
                            } else {
                                format!("{env}=\"{val}\"")
                            }
                        } else if QUOTED_NUM_REGEX.captures(val).is_some() {
                            let val = &val[1..val.len() - 1];
                            format!("{env}={val}")
                        } else {
                            format!("{env}={val}")
                        }
                    })
                    .collect::<Vec<_>>(),
            ),
        }
    }
    pub fn get_service_depends_on(&self, service: &Mapping) -> Option<Vec<String>> {
        let depends = service.get("depends_on")?;
        match depends.as_sequence() {
            Some(seq) => Some(
                seq.iter()
                    .map(|el| el.as_str().unwrap_or(""))
                    .filter(|o| !o.is_empty())
                    .map(String::from)
                    .collect::<Vec<_>>(),
            ),
            None => Some(
                depends
                    .as_mapping()
                    .unwrap_or(&EMPTY_MAP)
                    .keys()
                    .map(|k| k.as_str().unwrap_or(""))
                    .filter(|o| !o.is_empty())
                    .map(String::from)
                    .collect::<Vec<_>>(),
            ),
        }
    }
}
static COMPOSE_PATHS: [&str; 4] = [
    "compose.yaml",
    "compose.yml",
    "docker-compose.yaml",
    "docker-compose.yml",
];
pub fn get_compose_filename(
    filename: Option<&str>,
    verbosity: Verbosity,
) -> Result<String, String> {
    match filename {
        Some(name) => {
            if Path::new(&name).exists() {
                Ok(String::from(name))
            } else {
                Err(format!(
                    "{}: {}: no such file or directory",
                    "ERROR".red(),
                    name
                ))
            }
        }
        None => {
            let files = COMPOSE_PATHS.into_iter().filter(|f| Path::new(f).exists());
            let files_count = files.clone().count();
            match files_count {
                0 => Err(format!(
                    "Can't find a suitable configuration file in this directory.\n\
                    Are you in the right directory?\n\n\
                    Supported filenames: {}",
                    COMPOSE_PATHS.into_iter().collect::<Vec<&str>>().join(", ")
                )),
                1 => {
                    let filename_0 = files.map(String::from).next().unwrap();
                    if matches!(verbosity, Verbosity::Verbose) {
                        eprintln!("{}: Filename not provided", "DEBUG".green());
                        eprintln!("{}: Using {}", "DEBUG".green(), filename_0);
                    }
                    Ok(filename_0)
                }
                _ => {
                    let filenames = files.into_iter().collect::<Vec<&str>>();
                    let filename = filenames.first().map(|s| s.to_string()).unwrap();
                    if !matches!(verbosity, Verbosity::Quiet) {
                        eprintln!(
                            "{}: Found multiple config files with supported names: {}\n\
                            {}: Using {}",
                            "WARN".yellow(),
                            filenames.join(", "),
                            "WARN".yellow(),
                            filename
                        );
                    }
                    Ok(filename)
                }
            }
        }
    }
}