use std::env;
use std::ffi::OsString;
use std::fs;
use std::os::unix::fs::symlink;
use std::path::Path;
use std::process::Command;
use std::time::Duration;
use crossbeam_channel::Sender;
use regex::Regex;
use serde_derive::Deserialize;
use crate::blocks::{Block, ConfigBlock, Update};
use crate::config::SharedConfig;
use crate::de::deserialize_duration;
use crate::errors::*;
use crate::formatting::value::Value;
use crate::formatting::FormatTemplate;
use crate::input::{I3BarEvent, MouseButton};
use crate::scheduler::Task;
use crate::util::has_command;
use crate::widgets::text::TextWidget;
use crate::widgets::{I3BarWidget, State};
pub struct Pacman {
    id: usize,
    output: TextWidget,
    update_interval: Duration,
    format: FormatTemplate,
    format_singular: FormatTemplate,
    format_up_to_date: FormatTemplate,
    warning_updates_regex: Option<Regex>,
    critical_updates_regex: Option<Regex>,
    watched: Watched,
    uptodate: bool,
    hide_when_uptodate: bool,
}
#[derive(Debug, PartialEq, Eq)]
pub enum Watched {
    None,
    Pacman,
    
    AUR(String),
    
    Both(String),
}
#[derive(Deserialize, Debug, Clone)]
#[serde(deny_unknown_fields, default)]
pub struct PacmanConfig {
    
    #[serde(deserialize_with = "deserialize_duration")]
    pub interval: Duration,
    
    pub format: String,
    
    pub format_singular: String,
    
    pub format_up_to_date: String,
    
    
    
    pub warning_updates_regex: Option<String>,
    
    
    pub critical_updates_regex: Option<String>,
    
    pub aur_command: Option<String>,
    pub hide_when_uptodate: bool,
}
impl Default for PacmanConfig {
    fn default() -> Self {
        Self {
            interval: Duration::from_secs(600),
            format: "{pacman}".to_string(),
            format_singular: "{pacman}".to_string(),
            format_up_to_date: "{pacman}".to_string(),
            warning_updates_regex: None,
            critical_updates_regex: None,
            aur_command: None,
            hide_when_uptodate: false,
        }
    }
}
impl PacmanConfig {
    fn watched(
        format: &str,
        format_singular: &str,
        format_up_to_date: &str,
        aur_command: Option<String>,
    ) -> Result<Watched> {
        let aur_format = "{aur";
        let pacman_format = "{pacman";
        let both_format = "{both";
        let pacman_deprecated_format = "{count";
        let concatenated_format_str = format!("{}{}{}", format, format_singular, format_up_to_date);
        let aur = concatenated_format_str.contains(aur_format);
        let pacman = concatenated_format_str.contains(pacman_format)
            || concatenated_format_str.contains(pacman_deprecated_format);
        let both = concatenated_format_str.contains(both_format);
        if both || (pacman && aur) {
            let aur_command = aur_command.block_error(
                "pacman",
                "{aur} or {both} found in format string but no aur_command supplied",
            )?;
            Ok(Watched::Both(aur_command))
        } else if pacman && !aur {
            Ok(Watched::Pacman)
        } else if !pacman && aur {
            let aur_command = aur_command.block_error(
                "pacman",
                "{aur} found in format string but no aur_command supplied",
            )?;
            Ok(Watched::AUR(aur_command))
        } else {
            Ok(Watched::None)
        }
    }
}
impl ConfigBlock for Pacman {
    type Config = PacmanConfig;
    fn new(
        id: usize,
        block_config: Self::Config,
        shared_config: SharedConfig,
        _tx_update_request: Sender<Task>,
    ) -> Result<Self> {
        let output = TextWidget::new(id, 0, shared_config).with_icon("update")?;
        Ok(Pacman {
            id,
            update_interval: block_config.interval,
            format: FormatTemplate::from_string(&block_config.format)
                .block_error("pacman", "Invalid format specified for pacman::format")?,
            format_singular: FormatTemplate::from_string(&block_config.format_singular)
                .block_error(
                    "pacman",
                    "Invalid format specified for pacman::format_singular",
                )?,
            format_up_to_date: FormatTemplate::from_string(&block_config.format_up_to_date)
                .block_error(
                    "pacman",
                    "Invalid format specified for pacman::format_up_to_date",
                )?,
            output,
            warning_updates_regex: match block_config.warning_updates_regex {
                None => None, 
                Some(regex_str) => {
                    let regex = Regex::new(regex_str.as_ref()).map_err(|_| {
                        ConfigurationError(
                            "pacman".to_string(),
                            "invalid warning updates regex".to_string(),
                        )
                    })?;
                    Some(regex)
                }
            },
            critical_updates_regex: match block_config.critical_updates_regex {
                None => None, 
                Some(regex_str) => {
                    let regex = Regex::new(regex_str.as_ref()).map_err(|_| {
                        ConfigurationError(
                            "pacman".to_string(),
                            "invalid critical updates regex".to_string(),
                        )
                    })?;
                    Some(regex)
                }
            },
            watched: PacmanConfig::watched(
                &block_config.format,
                &block_config.format_singular,
                &block_config.format_up_to_date,
                block_config.aur_command,
            )?,
            uptodate: false,
            hide_when_uptodate: block_config.hide_when_uptodate,
        })
    }
}
fn run_command(var: &str) -> Result<()> {
    Command::new("sh")
        .args(&["-c", var])
        .spawn()
        .block_error("pacman", &format!("Failed to run command '{}'", var))?
        .wait()
        .block_error("pacman", &format!("Failed to wait for command '{}'", var))
        .map(|_| ())
}
fn has_fake_root() -> Result<bool> {
    has_command("pacman", "fakeroot")
}
fn check_fakeroot_command_exists() -> Result<()> {
    if !has_fake_root()? {
        Err(BlockError(
            "pacman".to_string(),
            "fakeroot not found".to_string(),
        ))
    } else {
        Ok(())
    }
}
fn get_updates_db_dir() -> Result<String> {
    let tmp_dir = env::temp_dir()
        .into_os_string()
        .into_string()
        .block_error("pacman", "There's something wrong with your $TMP variable")?;
    let user = env::var_os("USER")
        .unwrap_or_else(|| OsString::from(""))
        .into_string()
        .block_error("pacman", "There's a problem with your $USER")?;
    env::var_os("CHECKUPDATES_DB")
        .unwrap_or_else(|| OsString::from(format!("{}/checkup-db-{}", tmp_dir, user)))
        .into_string()
        .block_error("pacman", "There's a problem with your $CHECKUPDATES_DB")
}
fn get_pacman_available_updates() -> Result<String> {
    let updates_db = get_updates_db_dir()?;
    
    let db_path = env::var_os("DBPath")
        .map(Into::into)
        .unwrap_or_else(|| Path::new("/var/lib/pacman/").to_path_buf());
    
    fs::create_dir_all(&updates_db).block_error(
        "pacman",
        &format!("Failed to create checkup-db path '{}'", updates_db),
    )?;
    
    let local_cache = Path::new(&updates_db).join("local");
    if !local_cache.exists() {
        symlink(db_path.join("local"), local_cache)
            .block_error("pacman", "Failed to created required symlink")?;
    }
    
    run_command(&format!(
        "fakeroot -- pacman -Sy --dbpath \"{}\" --logfile /dev/null &> /dev/null",
        updates_db
    ))?;
    
    String::from_utf8(
        Command::new("sh")
            .env("LC_ALL", "C")
            .args(&[
                "-c",
                &format!("fakeroot pacman -Qu --dbpath \"{}\"", updates_db),
            ])
            .output()
            .block_error("pacman", "There was a problem running the pacman commands")?
            .stdout,
    )
    .block_error(
        "pacman",
        "There was a problem while converting the output of the pacman command to a string",
    )
}
fn get_aur_available_updates(aur_command: &str) -> Result<String> {
    String::from_utf8(
        Command::new("sh")
            .args(&["-c", aur_command])
            .output()
            .block_error("pacman", &format!("aur command: {} failed", aur_command))?
            .stdout,
    )
    .block_error(
        "pacman",
        "There was a problem while converting the aur command output to a string",
    )
}
fn get_update_count(updates: &str) -> usize {
    updates
        .lines()
        .filter(|line| !line.contains("[ignored]"))
        .count()
}
fn has_warning_update(updates: &str, regex: &Regex) -> bool {
    updates.lines().filter(|line| regex.is_match(line)).count() > 0
}
fn has_critical_update(updates: &str, regex: &Regex) -> bool {
    updates.lines().filter(|line| regex.is_match(line)).count() > 0
}
impl Block for Pacman {
    fn id(&self) -> usize {
        self.id
    }
    fn view(&self) -> Vec<&dyn I3BarWidget> {
        if self.uptodate && self.hide_when_uptodate {
            vec![]
        } else {
            vec![&self.output]
        }
    }
    fn update(&mut self) -> Result<Option<Update>> {
        let (formatting_map, warning, critical, cum_count) = match &self.watched {
            Watched::Pacman => {
                check_fakeroot_command_exists()?;
                let pacman_available_updates = get_pacman_available_updates()?;
                let pacman_count = get_update_count(&pacman_available_updates);
                let formatting_map = map!(
                    "count" => Value::from_integer(pacman_count as i64),
                    "pacman" => Value::from_integer(pacman_count as i64),
                );
                let warning = self.warning_updates_regex.as_ref().map_or(false, |regex| {
                    has_warning_update(&pacman_available_updates, regex)
                });
                let critical = self.critical_updates_regex.as_ref().map_or(false, |regex| {
                    has_critical_update(&pacman_available_updates, regex)
                });
                (formatting_map, warning, critical, pacman_count)
            }
            Watched::AUR(aur_command) => {
                let aur_available_updates = get_aur_available_updates(&aur_command)?;
                let aur_count = get_update_count(&aur_available_updates);
                let formatting_map = map!(
                    "aur" => Value::from_integer(aur_count as i64)
                );
                let warning = self.warning_updates_regex.as_ref().map_or(false, |regex| {
                    has_warning_update(&aur_available_updates, regex)
                });
                let critical = self.critical_updates_regex.as_ref().map_or(false, |regex| {
                    has_critical_update(&aur_available_updates, regex)
                });
                (formatting_map, warning, critical, aur_count)
            }
            Watched::Both(aur_command) => {
                check_fakeroot_command_exists()?;
                let pacman_available_updates = get_pacman_available_updates()?;
                let aur_available_updates = get_aur_available_updates(&aur_command)?;
                let pacman_count = get_update_count(&pacman_available_updates);
                let aur_count = get_update_count(&aur_available_updates);
                let formatting_map = map!(
                    "count" =>  Value::from_integer(pacman_count as i64),
                    "pacman" => Value::from_integer(pacman_count as i64),
                    "aur" =>    Value::from_integer(aur_count as i64),
                    "both" =>   Value::from_integer((pacman_count + aur_count) as i64),
                );
                let warning = self.warning_updates_regex.as_ref().map_or(false, |regex| {
                    has_warning_update(&aur_available_updates, regex)
                        || has_warning_update(&pacman_available_updates, regex)
                });
                let critical = self.critical_updates_regex.as_ref().map_or(false, |regex| {
                    has_critical_update(&aur_available_updates, regex)
                        || has_critical_update(&pacman_available_updates, regex)
                });
                (formatting_map, warning, critical, pacman_count + aur_count)
            }
            Watched::None => (std::collections::HashMap::new(), false, false, 0),
        };
        self.output.set_text(match cum_count {
            0 => self.format_up_to_date.render(&formatting_map)?,
            1 => self.format_singular.render(&formatting_map)?,
            _ => self.format.render(&formatting_map)?,
        });
        self.output.set_state(match cum_count {
            0 => State::Idle,
            _ => {
                if critical {
                    State::Critical
                } else if warning {
                    State::Warning
                } else {
                    State::Info
                }
            }
        });
        self.uptodate = cum_count == 0;
        Ok(Some(self.update_interval.into()))
    }
    fn click(&mut self, event: &I3BarEvent) -> Result<()> {
        if let MouseButton::Left = event.button {
            self.update()?;
        }
        Ok(())
    }
}
#[cfg(test)]
mod tests {
    use crate::blocks::pacman::{
        get_aur_available_updates, get_update_count, PacmanConfig, Watched,
    };
    #[test]
    fn test_get_update_count() {
        let no_update = "";
        assert_eq!(get_update_count(no_update), 0);
        let two_updates_available = concat!(
            "systemd 245.4-2 -> 245.5-1\n",
            "systemd-libs 245.4-2 -> 245.5-1\n"
        );
        assert_eq!(get_update_count(two_updates_available), 2);
    }
    #[test]
    fn test_watched() {
        let watched = PacmanConfig::watched("foo {count} bar", "foo {count} bar", "", None);
        assert!(watched.is_ok());
        assert_eq!(watched.unwrap(), Watched::Pacman);
        let watched = PacmanConfig::watched("foo {pacman} bar", "foo {pacman} bar", "", None);
        assert!(watched.is_ok());
        assert_eq!(watched.unwrap(), Watched::Pacman);
        let watched = PacmanConfig::watched("foo bar", "foo bar", "", None);
        assert!(watched.is_ok()); 
        let watched = PacmanConfig::watched("foo bar", "foo bar", "", Some("aur cmd".to_string()));
        assert!(watched.is_ok()); 
        let watched = PacmanConfig::watched(
            "foo {aur} bar",
            "foo {aur} bar",
            "",
            Some("aur cmd".to_string()),
        );
        assert!(watched.is_ok());
        assert_eq!(watched.unwrap(), Watched::AUR("aur cmd".to_string()));
        let watched = PacmanConfig::watched(
            "foo {pacman} {aur} bar",
            "foo {pacman} {aur} bar",
            "",
            Some("aur cmd".to_string()),
        );
        assert!(watched.is_ok());
        assert_eq!(watched.unwrap(), Watched::Both("aur cmd".to_string()));
        let watched =
            PacmanConfig::watched("foo {pacman} {aur} bar", "foo {pacman} {aur} bar", "", None);
        assert!(watched.is_err()); 
        let watched = PacmanConfig::watched("foo {both} bar", "foo {both} bar", "", None);
        assert!(watched.is_err()); 
        let watched = PacmanConfig::watched(
            "foo {both} bar",
            "foo {both} bar",
            "",
            Some("aur cmd".to_string()),
        );
        assert!(watched.is_ok());
        assert_eq!(watched.unwrap(), Watched::Both("aur cmd".to_string()));
    }
    #[test]
    fn test_get_aur_available_updates() {
        
        let updates = "foo x.x -> y.y\nbar x.x -> y.y\n";
        let aur_command = format!("printf '{}'", updates);
        let available_updates = get_aur_available_updates(&aur_command);
        assert!(available_updates.is_ok());
        assert_eq!(available_updates.unwrap(), updates);
    }
}