use std::ffi::OsString;
use std::fmt::Debug;
use std::io;
use std::io::IsTerminal;
use std::num::NonZero;
use std::path::PathBuf;
use std::str::FromStr;
#[cfg(feature = "test")]
use std::{
    collections::HashMap,
    io::Cursor,
    path::Path,
    sync::{Arc, Mutex},
};
use std::{env, thread};

use anstream::ColorChoice;
use anyhow::{Context, Result, bail};
use indicatif::ProgressDrawTarget;
#[cfg(feature = "test")]
use tracing::subscriber::DefaultGuard;
#[cfg(feature = "test")]
use tracing_subscriber::util::SubscriberInitExt;
#[cfg(feature = "test")]
use tracing_subscriber::{EnvFilter, Registry, reload::Handle};

#[cfg(feature = "test")]
use crate::cli::log;

mod file_source;
mod terminal_source;
pub use terminal_source::ColorableTerminal;

/// Allows concrete types for the process abstraction.
#[derive(Clone, Debug)]
pub enum Process {
    OsProcess(OsProcess),
    #[cfg(feature = "test")]
    TestProcess(TestContext),
}

impl Process {
    pub fn os() -> Self {
        Self::OsProcess(OsProcess::new())
    }

    pub fn name(&self) -> Option<String> {
        let arg0 = match self.var("RUSTUP_FORCE_ARG0") {
            Ok(v) => Some(v),
            Err(_) => self.args().next(),
        }
        .map(PathBuf::from);

        arg0.as_ref()
            .and_then(|a| a.file_stem())
            .and_then(std::ffi::OsStr::to_str)
            .map(String::from)
    }

    pub(crate) fn home_dir(&self) -> Option<PathBuf> {
        home::env::home_dir_with_env(self)
    }

    pub(crate) fn cargo_home(&self) -> Result<PathBuf> {
        home::env::cargo_home_with_env(self).context("failed to determine cargo home")
    }

    pub(crate) fn rustup_home(&self) -> Result<PathBuf> {
        home::env::rustup_home_with_env(self).context("failed to determine rustup home dir")
    }

    pub fn io_thread_count(&self) -> Result<IoThreadCount> {
        if let Ok(n) = self.var("RUSTUP_IO_THREADS") {
            let threads = usize::from_str(&n).context(
                "invalid value in RUSTUP_IO_THREADS -- must be a natural number greater than zero",
            )?;
            match threads {
                0 => bail!("RUSTUP_IO_THREADS must be a natural number greater than zero"),
                _ => return Ok(IoThreadCount::UserSpecified(threads)),
            }
        };

        let count = match thread::available_parallelism() {
            // Don't spawn more than 8 I/O threads unless the user tells us to.
            // Feel free to increase this value if it improves performance.
            Ok(threads) => Ord::min(threads.get(), 8),
            // Unknown for target platform or no permission to query.
            Err(_) => 1,
        };
        Ok(IoThreadCount::Default(count))
    }

    pub(crate) fn unpack_ram(&self) -> Result<Option<usize>, env::VarError> {
        Ok(match self.var_opt("RUSTUP_UNPACK_RAM")? {
            Some(budget) => usize::from_str(&budget).ok(),
            None => None,
        })
    }

    pub fn var_opt(&self, key: &str) -> Result<Option<String>, env::VarError> {
        match self.var(key) {
            Ok(val) => Ok(Some(val)),
            Err(env::VarError::NotPresent) => Ok(None),
            Err(err) => Err(err),
        }
    }

    pub fn var(&self, key: &str) -> Result<String, env::VarError> {
        let value = match self {
            Process::OsProcess(_) => env::var(key)?,
            #[cfg(feature = "test")]
            Process::TestProcess(p) => match p.vars.get(key) {
                Some(val) => val.to_owned(),
                None => return Err(env::VarError::NotPresent),
            },
        };

        match value.is_empty() {
            false => Ok(value),
            true => Err(env::VarError::NotPresent),
        }
    }

    #[cfg(not(target_os = "linux"))]
    pub fn permit_copy_rename(&self) -> bool {
        false
    }

    #[cfg(target_os = "linux")]
    pub fn permit_copy_rename(&self) -> bool {
        match self {
            Process::OsProcess(_) => env::var_os("RUSTUP_PERMIT_COPY_RENAME").is_some(),
            #[cfg(feature = "test")]
            Process::TestProcess(p) => p.vars.contains_key("RUSTUP_PERMIT_COPY_RENAME"),
        }
    }

    pub(crate) fn var_os(&self, key: &str) -> Option<OsString> {
        let value = match self {
            Process::OsProcess(_) => env::var_os(key)?,
            #[cfg(feature = "test")]
            Process::TestProcess(p) => p.vars.get(key).map(OsString::from)?,
        };

        match value.is_empty() {
            false => Some(value),
            true => None,
        }
    }

    pub(crate) fn args(&self) -> Box<dyn Iterator<Item = String> + '_> {
        match self {
            Process::OsProcess(_) => Box::new(env::args()),
            #[cfg(feature = "test")]
            Process::TestProcess(p) => Box::new(p.args.iter().cloned()),
        }
    }

    pub(crate) fn args_os(&self) -> Box<dyn Iterator<Item = OsString> + '_> {
        match self {
            Process::OsProcess(_) => Box::new(env::args_os()),
            #[cfg(feature = "test")]
            Process::TestProcess(p) => Box::new(p.args.iter().map(OsString::from)),
        }
    }

    pub(crate) fn stdin(&self) -> Box<dyn file_source::Stdin> {
        match self {
            Process::OsProcess(_) => Box::new(io::stdin()),
            #[cfg(feature = "test")]
            Process::TestProcess(p) => Box::new(file_source::TestStdin(p.stdin.clone())),
        }
    }

    pub(crate) fn stdout(&self) -> ColorableTerminal {
        match self {
            Process::OsProcess(_) => ColorableTerminal::stdout(self),
            #[cfg(feature = "test")]
            Process::TestProcess(p) => {
                ColorableTerminal::test(file_source::TestWriter(p.stdout.clone()), self)
            }
        }
    }

    pub(crate) fn stderr(&self) -> ColorableTerminal {
        match self {
            Process::OsProcess(_) => ColorableTerminal::stderr(self),
            #[cfg(feature = "test")]
            Process::TestProcess(p) => {
                ColorableTerminal::test(file_source::TestWriter(p.stderr.clone()), self)
            }
        }
    }

    pub fn current_dir(&self) -> io::Result<PathBuf> {
        match self {
            Process::OsProcess(_) => env::current_dir(),
            #[cfg(feature = "test")]
            Process::TestProcess(p) => Ok(p.cwd.clone()),
        }
    }

    pub fn progress_draw_target(&self) -> ProgressDrawTarget {
        match self {
            Process::OsProcess(_) => (),
            #[cfg(feature = "test")]
            Process::TestProcess(_) => return ProgressDrawTarget::hidden(),
        }

        let term = self.stdout();
        let term = match self.var("RUSTUP_TERM_PROGRESS_WHEN") {
            Ok(s) if s.eq_ignore_ascii_case("always") => Some(term),
            Ok(s) if s.eq_ignore_ascii_case("never") => None,
            _ if term.is_a_tty() => Some(term),
            _ => None,
        };

        match term {
            Some(t) => ProgressDrawTarget::term_like_with_hz(Box::new(t), 20),
            None => ProgressDrawTarget::hidden(),
        }
    }

    fn color_choice(&self, is_a_tty: bool) -> ColorChoice {
        match self.var("RUSTUP_TERM_COLOR") {
            Ok(s) if s.eq_ignore_ascii_case("always") => ColorChoice::Always,
            Ok(s) if s.eq_ignore_ascii_case("never") => ColorChoice::Never,
            _ if is_a_tty => ColorChoice::Auto,
            _ => ColorChoice::Never,
        }
    }

    pub fn concurrent_downloads(&self) -> Option<usize> {
        let s = self.var("RUSTUP_CONCURRENT_DOWNLOADS").ok()?;
        Some(NonZero::from_str(&s).ok()?.get())
    }
}

pub enum IoThreadCount {
    Default(usize),
    UserSpecified(usize),
}

impl From<IoThreadCount> for usize {
    fn from(c: IoThreadCount) -> Self {
        match c {
            IoThreadCount::Default(n) | IoThreadCount::UserSpecified(n) => n,
        }
    }
}

impl home::env::Env for Process {
    fn home_dir(&self) -> Option<PathBuf> {
        match self {
            Process::OsProcess(_) => home::env::OS_ENV.home_dir(),
            #[cfg(feature = "test")]
            Process::TestProcess(_) => self.var("HOME").ok().map(|v| v.into()),
        }
    }

    fn current_dir(&self) -> Result<PathBuf, io::Error> {
        match self {
            Process::OsProcess(_) => home::env::OS_ENV.current_dir(),
            #[cfg(feature = "test")]
            Process::TestProcess(_) => self.current_dir(),
        }
    }

    fn var_os(&self, key: &str) -> Option<OsString> {
        self.var_os(key)
    }
}

// ----------- real process -----------------

#[derive(Clone, Debug)]
pub struct OsProcess {
    pub(self) stderr_is_a_tty: bool,
    pub(self) stdout_is_a_tty: bool,
}

impl OsProcess {
    pub fn new() -> Self {
        OsProcess {
            stderr_is_a_tty: io::stderr().is_terminal(),
            stdout_is_a_tty: io::stdout().is_terminal(),
        }
    }
}

impl Default for OsProcess {
    fn default() -> Self {
        OsProcess::new()
    }
}

// ------------ test process ----------------

#[cfg(feature = "test")]
pub struct TestProcess {
    pub process: Process,
    pub console_filter: Handle<EnvFilter, Registry>,
    // These guards are dropped _in order_ at the end of the test.
    #[cfg(feature = "otel")]
    _telemetry_guard: log::GlobalTelemetryGuard,
    _tracing_guard: DefaultGuard,
}

#[cfg(feature = "test")]
impl TestProcess {
    pub fn new<P: AsRef<Path>, A: AsRef<str>>(
        cwd: P,
        args: &[A],
        vars: HashMap<String, String>,
        stdin: &str,
    ) -> Self {
        Self::from(TestContext {
            cwd: cwd.as_ref().to_path_buf(),
            args: args.iter().map(|s| s.as_ref().to_string()).collect(),
            vars,
            stdin: Arc::new(Mutex::new(Cursor::new(stdin.to_string()))),
            stdout: Arc::default(),
            stderr: Arc::default(),
        })
    }

    pub fn with_vars(vars: HashMap<String, String>) -> Self {
        Self::from(TestContext {
            vars,
            ..Default::default()
        })
    }

    /// Extracts the stdout from the process
    pub fn stdout(&self) -> Vec<u8> {
        let tp = match &self.process {
            Process::TestProcess(tp) => tp,
            _ => unreachable!(),
        };

        tp.stdout.lock().unwrap_or_else(|e| e.into_inner()).clone()
    }

    /// Extracts the stderr from the process
    pub fn stderr(&self) -> Vec<u8> {
        let tp = match &self.process {
            Process::TestProcess(tp) => tp,
            _ => unreachable!(),
        };

        tp.stderr.lock().unwrap_or_else(|e| e.into_inner()).clone()
    }
}

#[cfg(feature = "test")]
impl From<TestContext> for TestProcess {
    fn from(inner: TestContext) -> Self {
        let inner = Process::TestProcess(inner);
        let (tracing_subscriber, console_filter) = log::tracing_subscriber(&inner);
        Self {
            process: inner,
            console_filter,
            #[cfg(feature = "otel")]
            _telemetry_guard: log::set_global_telemetry(),
            _tracing_guard: tracing_subscriber.set_default(),
        }
    }
}

#[cfg(feature = "test")]
impl Default for TestProcess {
    fn default() -> Self {
        Self::from(TestContext::default())
    }
}

#[cfg(feature = "test")]
#[derive(Clone, Debug, Default)]
pub struct TestContext {
    pub cwd: PathBuf,
    args: Vec<String>,
    vars: HashMap<String, String>,
    stdin: file_source::TestStdinInner,
    stdout: file_source::TestWriterInner,
    stderr: file_source::TestWriterInner,
}

#[cfg(test)]
mod tests {
    use std::collections::HashMap;

    use super::*;
    use crate::process::TestProcess;
    use crate::test::Env;

    #[test]
    fn term_color_choice() {
        fn assert_color_choice(env_val: &str, is_a_tty: bool, color_choice: ColorChoice) {
            let mut vars = HashMap::new();
            vars.env("RUSTUP_TERM_COLOR", env_val);
            let tp = TestProcess::with_vars(vars);
            assert_eq!(tp.process.color_choice(is_a_tty), color_choice);
        }

        assert_color_choice("aLWayS", false, ColorChoice::Always);
        assert_color_choice("neVer", false, ColorChoice::Never);
        // tty + `auto` enables the colors.
        assert_color_choice("AutO", true, ColorChoice::Auto);
        // non-tty + `auto` does not enable the colors.
        assert_color_choice("aUTo", false, ColorChoice::Never);
    }
}
