use std::collections::HashMap;
use std::io::{BufRead, BufReader, Write};
use std::path::PathBuf;
use std::process::{Command as CommandSys, Stdio};
use std::sync::atomic::Ordering;
use std::sync::mpsc;

use nu_engine::env_to_strings;
use nu_protocol::engine::{EngineState, Stack};
use nu_protocol::{ast::Call, engine::Command, ShellError, Signature, SyntaxShape, Value};
use nu_protocol::{Category, Example, ListStream, PipelineData, RawStream, Span, Spanned};

use itertools::Itertools;

use nu_engine::CallExt;
use pathdiff::diff_paths;
use regex::Regex;

const OUTPUT_BUFFER_SIZE: usize = 1024;
const OUTPUT_BUFFERS_IN_FLIGHT: usize = 3;

#[derive(Clone)]
pub struct External;

impl Command for External {
    fn name(&self) -> &str {
        "run-external"
    }

    fn usage(&self) -> &str {
        "Runs external command"
    }

    fn signature(&self) -> nu_protocol::Signature {
        Signature::build(self.name())
            .switch("redirect-stdout", "redirect-stdout", None)
            .switch("redirect-stderr", "redirect-stderr", None)
            .rest("rest", SyntaxShape::Any, "external command to run")
            .category(Category::System)
    }

    fn run(
        &self,
        engine_state: &EngineState,
        stack: &mut Stack,
        call: &Call,
        input: PipelineData,
    ) -> Result<PipelineData, ShellError> {
        let name: Spanned<String> = call.req(engine_state, stack, 0)?;
        let args: Vec<Value> = call.rest(engine_state, stack, 1)?;
        let redirect_stdout = call.has_flag("redirect-stdout");
        let redirect_stderr = call.has_flag("redirect-stderr");

        // Translate environment variables from Values to Strings
        let env_vars_str = env_to_strings(engine_state, stack)?;

        fn value_as_spanned(value: Value) -> Result<Spanned<String>, ShellError> {
            let span = value.span()?;

            value
                .as_string()
                .map(|item| Spanned { item, span })
                .map_err(|_| {
                    ShellError::ExternalCommand(
                        "Cannot convert argument to a string".into(),
                        "All arguments to an external command need to be string-compatible".into(),
                        span,
                    )
                })
        }

        let args = args
            .into_iter()
            .flat_map(|arg| match arg {
                Value::List { vals, .. } => vals
                    .into_iter()
                    .map(value_as_spanned)
                    .collect::<Vec<Result<Spanned<String>, ShellError>>>(),
                val => vec![value_as_spanned(val)],
            })
            .collect::<Result<Vec<Spanned<String>>, ShellError>>()?;

        let command = ExternalCommand {
            name,
            args,
            redirect_stdout,
            redirect_stderr,
            env_vars: env_vars_str,
        };
        command.run_with_input(engine_state, stack, input)
    }

    fn examples(&self) -> Vec<Example> {
        vec![Example {
            description: "Run an external command",
            example: r#"run-external "echo" "-n" "hello""#,
            result: None,
        }]
    }
}

pub struct ExternalCommand {
    pub name: Spanned<String>,
    pub args: Vec<Spanned<String>>,
    pub redirect_stdout: bool,
    pub redirect_stderr: bool,
    pub env_vars: HashMap<String, String>,
}

impl ExternalCommand {
    pub fn run_with_input(
        &self,
        engine_state: &EngineState,
        stack: &mut Stack,
        input: PipelineData,
    ) -> Result<PipelineData, ShellError> {
        let head = self.name.span;

        let ctrlc = engine_state.ctrlc.clone();

        let mut process = self.create_process(&input, false, head)?;
        let child;

        #[cfg(windows)]
        {
            match process.spawn() {
                Err(_) => {
                    let mut process = self.create_process(&input, true, head)?;
                    child = process.spawn();
                }
                Ok(process) => {
                    child = Ok(process);
                }
            }
        }

        #[cfg(not(windows))]
        {
            child = process.spawn()
        }

        match child {
            Err(err) => Err(ShellError::ExternalCommand(
                "can't run executable".to_string(),
                err.to_string(),
                self.name.span,
            )),
            Ok(mut child) => {
                if !input.is_nothing() {
                    let engine_state = engine_state.clone();
                    let mut stack = stack.clone();
                    stack.update_config(
                        "use_ansi_coloring",
                        Value::Bool {
                            val: false,
                            span: Span::new(0, 0),
                        },
                    );
                    // if there is a string or a stream, that is sent to the pipe std
                    if let Some(mut stdin_write) = child.stdin.take() {
                        std::thread::spawn(move || {
                            let input = crate::Table::run(
                                &crate::Table,
                                &engine_state,
                                &mut stack,
                                &Call::new(head),
                                input,
                            );

                            if let Ok(input) = input {
                                for value in input.into_iter() {
                                    let buf = match value {
                                        Value::String { val, .. } => val.into_bytes(),
                                        Value::Binary { val, .. } => val,
                                        _ => return Err(()),
                                    };
                                    if stdin_write.write(&buf).is_err() {
                                        return Ok(());
                                    }
                                }
                            }

                            Ok(())
                        });
                    }
                }

                let redirect_stdout = self.redirect_stdout;
                let redirect_stderr = self.redirect_stderr;
                let span = self.name.span;
                let output_ctrlc = ctrlc.clone();
                let (stdout_tx, stdout_rx) = mpsc::sync_channel(OUTPUT_BUFFERS_IN_FLIGHT);
                let (stderr_tx, stderr_rx) = mpsc::sync_channel(OUTPUT_BUFFERS_IN_FLIGHT);
                let (exit_code_tx, exit_code_rx) = mpsc::channel();

                std::thread::spawn(move || {
                    // If this external is not the last expression, then its output is piped to a channel
                    // and we create a ListStream that can be consumed

                    if redirect_stderr {
                        let stderr = child.stderr.take().ok_or_else(|| {
                            ShellError::ExternalCommand(
                                "Error taking stderr from external".to_string(),
                                "Redirects need access to stderr of an external command"
                                    .to_string(),
                                span,
                            )
                        })?;

                        // Stderr is read using the Buffer reader. It will do so until there is an
                        // error or there are no more bytes to read
                        let mut buf_read = BufReader::with_capacity(OUTPUT_BUFFER_SIZE, stderr);
                        while let Ok(bytes) = buf_read.fill_buf() {
                            if bytes.is_empty() {
                                break;
                            }

                            // The Cow generated from the function represents the conversion
                            // from bytes to String. If no replacements are required, then the
                            // borrowed value is a proper UTF-8 string. The Owned option represents
                            // a string where the values had to be replaced, thus marking it as bytes
                            let bytes = bytes.to_vec();
                            let length = bytes.len();
                            buf_read.consume(length);

                            if let Some(ctrlc) = &ctrlc {
                                if ctrlc.load(Ordering::SeqCst) {
                                    break;
                                }
                            }

                            match stderr_tx.send(bytes) {
                                Ok(_) => continue,
                                Err(_) => break,
                            }
                        }
                    }

                    if redirect_stdout {
                        let stdout = child.stdout.take().ok_or_else(|| {
                            ShellError::ExternalCommand(
                                "Error taking stdout from external".to_string(),
                                "Redirects need access to stdout of an external command"
                                    .to_string(),
                                span,
                            )
                        })?;

                        // Stdout is read using the Buffer reader. It will do so until there is an
                        // error or there are no more bytes to read
                        let mut buf_read = BufReader::with_capacity(OUTPUT_BUFFER_SIZE, stdout);
                        while let Ok(bytes) = buf_read.fill_buf() {
                            if bytes.is_empty() {
                                break;
                            }

                            // The Cow generated from the function represents the conversion
                            // from bytes to String. If no replacements are required, then the
                            // borrowed value is a proper UTF-8 string. The Owned option represents
                            // a string where the values had to be replaced, thus marking it as bytes
                            let bytes = bytes.to_vec();
                            let length = bytes.len();
                            buf_read.consume(length);

                            if let Some(ctrlc) = &ctrlc {
                                if ctrlc.load(Ordering::SeqCst) {
                                    break;
                                }
                            }

                            match stdout_tx.send(bytes) {
                                Ok(_) => continue,
                                Err(_) => break,
                            }
                        }
                    }

                    match child.wait() {
                        Err(err) => Err(ShellError::ExternalCommand(
                            "External command exited with error".into(),
                            err.to_string(),
                            span,
                        )),
                        Ok(x) => {
                            if let Some(code) = x.code() {
                                let _ = exit_code_tx.send(Value::Int {
                                    val: code as i64,
                                    span: head,
                                });
                            } else if x.success() {
                                let _ = exit_code_tx.send(Value::Int { val: 0, span: head });
                            } else {
                                let _ = exit_code_tx.send(Value::Int {
                                    val: -1,
                                    span: head,
                                });
                            }
                            Ok(())
                        }
                    }
                });
                let stdout_receiver = ChannelReceiver::new(stdout_rx);
                let stderr_receiver = ChannelReceiver::new(stderr_rx);
                let exit_code_receiver = ValueReceiver::new(exit_code_rx);

                Ok(PipelineData::ExternalStream {
                    stdout: if redirect_stdout {
                        Some(RawStream::new(
                            Box::new(stdout_receiver),
                            output_ctrlc.clone(),
                            head,
                        ))
                    } else {
                        None
                    },
                    stderr: Some(RawStream::new(
                        Box::new(stderr_receiver),
                        output_ctrlc.clone(),
                        head,
                    )),
                    exit_code: Some(ListStream::from_stream(
                        Box::new(exit_code_receiver),
                        output_ctrlc,
                    )),
                    span: head,
                    metadata: None,
                })
            }
        }
    }

    fn create_process(
        &self,
        input: &PipelineData,
        use_cmd: bool,
        span: Span,
    ) -> Result<CommandSys, ShellError> {
        let mut process = if let Some(d) = self.env_vars.get("PWD") {
            let mut process = if use_cmd {
                self.spawn_cmd_command()
            } else {
                self.create_command(d)?
            };

            process.current_dir(d);
            process
        } else {
            return Err(ShellError::SpannedLabeledErrorHelp(
                "Current directory not found".to_string(),
                "did not find PWD environment variable".to_string(),
                span,
                concat!(
                    "The environment variable 'PWD' was not found. ",
                    "It is required to define the current directory when running an external command."
                ).to_string(),
            ));
        };

        process.envs(&self.env_vars);

        // If the external is not the last command, its output will get piped
        // either as a string or binary
        if self.redirect_stdout {
            process.stdout(Stdio::piped());
        }

        if self.redirect_stderr {
            process.stderr(Stdio::piped());
        }

        // If there is an input from the pipeline. The stdin from the process
        // is piped so it can be used to send the input information
        if !matches!(input, PipelineData::Value(Value::Nothing { .. }, ..)) {
            process.stdin(Stdio::piped());
        }

        Ok(process)
    }

    fn create_command(&self, cwd: &str) -> Result<CommandSys, ShellError> {
        // in all the other cases shell out
        if cfg!(windows) {
            //TODO. This should be modifiable from the config file.
            // We could give the option to call from powershell
            // for minimal builds cwd is unused
            if self.name.item.ends_with(".cmd") || self.name.item.ends_with(".bat") {
                Ok(self.spawn_cmd_command())
            } else {
                self.spawn_simple_command(cwd)
            }
        } else if self.name.item.ends_with(".sh") {
            Ok(self.spawn_sh_command())
        } else {
            self.spawn_simple_command(cwd)
        }
    }

    /// Spawn a command without shelling out to an external shell
    pub fn spawn_simple_command(&self, cwd: &str) -> Result<std::process::Command, ShellError> {
        let is_path = |arg: &str| {
            let head = match arg.split_once(::std::path::is_separator) {
                Some((x, _)) => x,
                None => arg,
            };
            head == "~" || head.chars().all(|ch| ch == '.')
        };
        let expand_path = |arg: String| {
            if is_path(&arg) {
                nu_path::expand_path_with(arg, cwd)
                    .to_string_lossy()
                    .to_string()
            } else {
                arg
            }
        };

        let head = trim_enclosing_quotes(&self.name.item);
        let head = expand_path(head);

        let mut process = std::process::Command::new(&head);

        for arg in self.args.iter() {
            let mut arg = Spanned {
                item: trim_enclosing_quotes(&arg.item),
                span: arg.span,
            };
            arg.item = expand_path(arg.item);

            let cwd = PathBuf::from(cwd);

            if arg.item.contains('*') {
                if let Ok((prefix, matches)) = nu_engine::glob_from(&arg, &cwd, self.name.span) {
                    let matches: Vec<_> = matches.collect();

                    // FIXME: do we want to special-case this further? We might accidentally expand when they don't
                    // intend to
                    if matches.is_empty() {
                        process.arg(&arg.item);
                    }
                    for m in matches {
                        if let Ok(arg) = m {
                            let arg = if let Some(prefix) = &prefix {
                                if let Ok(remainder) = arg.strip_prefix(&prefix) {
                                    let new_prefix = if let Some(pfx) = diff_paths(&prefix, &cwd) {
                                        pfx
                                    } else {
                                        prefix.to_path_buf()
                                    };

                                    new_prefix.join(remainder).to_string_lossy().to_string()
                                } else {
                                    arg.to_string_lossy().to_string()
                                }
                            } else {
                                arg.to_string_lossy().to_string()
                            };

                            process.arg(&arg);
                        } else {
                            process.arg(&arg.item);
                        }
                    }
                }
            } else {
                process.arg(&arg.item);
            }
        }

        Ok(process)
    }

    /// Spawn a cmd command with `cmd /c args...`
    pub fn spawn_cmd_command(&self) -> std::process::Command {
        let mut process = std::process::Command::new("cmd");

        // Disable AutoRun
        // TODO: There should be a config option to enable/disable this
        // Alternatively (even better) a config option to specify all the arguments to pass to cmd
        process.arg("/D");

        process.arg("/c");
        process.arg(&self.name.item);
        for arg in &self.args {
            // Clean the args before we use them:
            // https://stackoverflow.com/questions/1200235/how-to-pass-a-quoted-pipe-character-to-cmd-exe
            // cmd.exe needs to have a caret to escape a pipe
            let arg = arg.item.replace('|', "^|");
            process.arg(&arg);
        }
        process
    }

    /// Spawn a sh command with `sh -c args...`
    pub fn spawn_sh_command(&self) -> std::process::Command {
        let joined_and_escaped_arguments = self
            .args
            .iter()
            .map(|arg| shell_arg_escape(&arg.item))
            .join(" ");
        let cmd_with_args = vec![self.name.item.clone(), joined_and_escaped_arguments].join(" ");
        let mut process = std::process::Command::new("sh");
        process.arg("-c").arg(cmd_with_args);
        process
    }
}

fn has_unsafe_shell_characters(arg: &str) -> bool {
    let re: Regex = Regex::new(r"[^\w@%+=:,./-]").expect("regex to be valid");

    re.is_match(arg)
}

fn shell_arg_escape(arg: &str) -> String {
    match arg {
        "" => String::from("''"),
        s if !has_unsafe_shell_characters(s) => String::from(s),
        _ => {
            let single_quotes_escaped = arg.split('\'').join("'\"'\"'");
            format!("'{}'", single_quotes_escaped)
        }
    }
}

fn trim_enclosing_quotes(input: &str) -> String {
    let mut chars = input.chars();

    match (chars.next(), chars.next_back()) {
        (Some('"'), Some('"')) => chars.collect(),
        (Some('\''), Some('\'')) => chars.collect(),
        _ => input.to_string(),
    }
}

// Receiver used for the RawStream
// It implements iterator so it can be used as a RawStream
struct ChannelReceiver {
    rx: mpsc::Receiver<Vec<u8>>,
}

impl ChannelReceiver {
    pub fn new(rx: mpsc::Receiver<Vec<u8>>) -> Self {
        Self { rx }
    }
}

impl Iterator for ChannelReceiver {
    type Item = Result<Vec<u8>, ShellError>;

    fn next(&mut self) -> Option<Self::Item> {
        match self.rx.recv() {
            Ok(v) => Some(Ok(v)),
            Err(_) => None,
        }
    }
}

// Receiver used for the ListStream
// It implements iterator so it can be used as a ListStream
struct ValueReceiver {
    rx: mpsc::Receiver<Value>,
}

impl ValueReceiver {
    pub fn new(rx: mpsc::Receiver<Value>) -> Self {
        Self { rx }
    }
}

impl Iterator for ValueReceiver {
    type Item = Value;

    fn next(&mut self) -> Option<Self::Item> {
        match self.rx.recv() {
            Ok(v) => Some(v),
            Err(_) => None,
        }
    }
}
