use crate::{Duration, Env, Error, ErrorKind, Paths, Result, Status};
use chrono::prelude::*;
use colored::*;
use log::{debug, info, warn};
use serde::{Deserialize, Serialize};
use std::cmp::Ordering;
use std::fmt::{Display, Formatter};
use std::path::{Path, PathBuf};
use std::time::Instant;

#[derive(
    Serialize, Deserialize, Clone, Debug, Hash, Default, PartialEq, Eq, juniper::GraphQLObject,
)]
/// Metadata about a std::process::Command
pub struct Command {
    /// The working directory
    pub dir: String,
    /// The command path
    pub path: String,
    /// The command name
    pub name: String,
    /// The command arguments
    pub args: Vec<String>,
    /// The standard output text
    pub stdout: String,
    /// The standard error text
    pub stderr: String,
    /// Indication of success or failure
    pub status: Status,
    /// The exit code of the process
    pub exit_code: i32,
    /// The duration of the command execution
    pub duration: Duration,
    /// The timeout duration for the command execution
    pub timeout: Duration,
    /// The start time of the command execution
    pub start: String,
    /// Environment metadata
    pub env: Env,
    /// String Tags
    pub tags: Vec<String>,
}

impl Command {
    pub fn new(command: &str) -> Command {
        let (name, args) = crate::text::parse_command(command);
        let mut c = Command::default();
        c.timeout = Duration::from_std(std::time::Duration::new(300, 0)).unwrap();
        c.name = name;
        c.args = args;
        c.dir = match std::env::current_dir() {
            Ok(dir) => dir.to_str().unwrap().to_string(),
            Err(_) => std::env::temp_dir().to_str().unwrap().to_string(),
        };
        c
    }

    pub fn open<P: AsRef<Path>>(path: P) -> Result<Command> {
        let json = std::fs::read_to_string(path.as_ref())?;
        Ok(serde_json::from_str::<Command>(&json)?)
    }

    pub fn save<P: AsRef<Path>>(&self, path: P) -> Result<()> {
        let json = serde_json::to_string_pretty(&self)?;
        std::fs::write(path.as_ref(), &json)?;
        Ok(())
    }

    pub fn start_utc(&self) -> DateTime<Utc> {
        match &self.start.parse::<DateTime<Utc>>() {
            Ok(dt) => *dt,
            Err(_) => Utc::now(),
        }
    }

    pub fn start_local(&self) -> DateTime<Local> {
        self.start_utc().with_timezone(&Local)
    }

    pub fn end_utc(&self) -> DateTime<Utc> {
        let chrono_duration = chrono::Duration::from_std(self.duration.to_std().unwrap()).unwrap();
        let end_time = self
            .start_utc()
            .checked_add_signed(chrono_duration)
            .unwrap();
        end_time
    }

    pub fn end_local(&self) -> DateTime<Local> {
        self.end_utc().with_timezone(&Local)
    }

    pub fn age(&self) -> std::time::Duration {
        let chrono_duration = Utc::now()
            .naive_utc()
            .signed_duration_since(self.start_utc().naive_utc());
        match chrono_duration.to_std() {
            Ok(duration) => duration,
            Err(_) => std::time::Duration::new(0, 0),
        }
    }

    pub fn get_tag_value(&self, name: &str) -> Option<String> {
        for tag in &self.tags {
            let prop_name = format!("{}=", name);
            if tag.contains(&prop_name) {
                return Some(tag.replace(&prop_name, ""));
            }
        }
        None
    }
    pub fn duration_string(&self) -> String {
        crate::duration::format(&self.duration.to_std().unwrap())
    }

    pub fn summary(&self) -> String {
        format!(
            "{} {} {} {}",
            &self.status.symbol(),
            &self.duration_string(),
            &self.name,
            &self.args.join(" ")
        )
    }

    pub fn matches(&self, pattern: &str) -> bool {
        if self.dir.contains(pattern) {
            return true;
        }
        if self.stdout.contains(pattern) {
            return true;
        }

        if self.stderr.contains(pattern) {
            return true;
        }
        for tag in &self.tags {
            if tag.contains(pattern) {
                return true;
            }
        }
        false
    }

    pub fn details(&self) -> String {
        format!(
            "{}\ndirectory: {}\nstart time: {}\noutput:\n{}\n{}",
            self.summary(),
            self.dir,
            self.start_local().to_string(),
            self.stdout,
            self.stderr
        )
    }

    pub fn exec_in<P: AsRef<Path>>(&self, path: P) -> Result<Command> {
        let mut c = self.clone();
        c.dir = path
            .as_ref()
            .to_path_buf()
            .into_os_string()
            .into_string()
            .unwrap();
        c.exec()
    }

    pub fn expect_exit_code(&self, exit_code: i32) -> Result<Command> {
        if self.exit_code == exit_code {
            Ok(self.clone()) //.clone())
        } else {
            Err(Error::from_kind(ErrorKind::IncorrectExitCode(
                exit_code,
                format!("{}", &self),
            )))
        }
    }
    // TODO: add timeout support: https://docs.rs/wait-timeout/0.2.0/wait_timeout/
    pub fn exec(&self) -> Result<Command> {
        let mut command = self.clone();
        let now = Instant::now();
        command.env = Env::default();

        let mut dir = PathBuf::new();
        dir.push(&self.dir);
        if !dir.exists() {
            return Err(Error::from_kind(ErrorKind::PathDoesNotExist(dir)));
        }

        let command_path = Env::which(&command.name)?;
        command.path = format!("{}", Paths::which(&command.name).unwrap().display());
        let mut process_command = std::process::Command::new(&command.path);
        command.start = Utc::now().to_string();
        process_command.current_dir(&command.dir);
        debug!("directory={}", &command.dir);
        debug!("command={} {}", &command.name, &command.args.join(" "));
        let ext = Paths::extension(&command_path);
        if ext == "bat" || ext == "cmd" {
            command.path = format!("{}", Paths::which("cmd").unwrap().display());
            process_command =
                std::process::Command::new(&format!("{}", Paths::which("cmd").unwrap().display()));
            process_command.current_dir(&command.dir);

            let mut args: Vec<String> = Vec::new();
            args.push("/C".to_string());
            args.push(format!("{}", command_path.display()));
            args.append(&mut command.args.clone());
            process_command.args(crate::text::get_vec_osstring(&args.clone()));
        } else {
            process_command.current_dir(&command.dir);
            process_command.args(crate::text::get_vec_osstring(&command.args.clone()));
        }

        command.status = Status::Ok;
        command.exit_code = 0;
        let output = process_command.output()?;
        command.exit_code = match output.status.code() {
            Some(code) => code,
            None => 0,
        };
        command.stdout = match std::str::from_utf8(&output.stdout) {
            Ok(text) => text.trim().to_string(),
            Err(_) => "* error parsing stdout *".to_string(),
        };

        command.stderr = match std::str::from_utf8(&output.stderr) {
            Ok(text) => text.trim().to_string(),
            Err(_) => "* error parsing stderr *".to_string(),
        };

        command.duration = Duration::from_std(now.elapsed()).unwrap();
        command.status = match output.status.success() {
            true => {
                info!("{}", &command.summary());
                Status::Ok
            }
            false => {
                warn!("{}", &command.summary());
                Status::Error
            }
        };

        Ok(command)
    }
}

impl Display for Command {
    fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
        let mut output_lines = Vec::new();
        if self.stdout.len() > 0 {
            output_lines.append(&mut self.stdout.split('\n').collect());
        }
        if self.stderr.len() > 0 {
            output_lines.append(&mut self.stderr.split('\n').collect());
        }
        write!(
            f,
            "{}{}{}{} {}\n",
            &self.dir.white().bold(),
            ">".white().bold(),
            "".clear(),
            &self.name.green(),
            &self.args.join(" "),
        )?;
        for line in output_lines {
            writeln!(f, " {} {}", "".cyan(), line.clear())?;
        }
        Ok(())
    }
}

impl PartialOrd for Command {
    fn partial_cmp(&self, other: &Command) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

impl Ord for Command {
    fn cmp(&self, other: &Command) -> Ordering {
        self.start_utc().cmp(&other.start_utc())
    }
}

#[cfg(test)]
//use std::path::PathBuf;
#[test]
fn usage() {
    let git_version = Command::new("git --version").exec().unwrap();
    assert_eq!(git_version.status, Status::Ok);
    assert!(git_version.stdout.contains("git version"));
    let dir = PathBuf::from(&git_version.dir);
    assert!(dir.exists(), "dir");
}
