use std::collections::BTreeMap;
use std::path::PathBuf;
use std::process::ExitStatus;
use std::time::Duration;

use thiserror::Error;

use super::capture::CapturedOutput;

#[derive(Clone, Debug)]
pub(crate) struct ProcessSpec {
    pub(super) label: &'static str,
    pub(super) command: String,
    pub(super) args: Vec<String>,
    pub(super) cwd: Option<PathBuf>,
    pub(super) env: BTreeMap<String, String>,
    pub(super) stdin: Option<ProcessStdin>,
    pub(super) timeout: Option<Duration>,
    pub(super) output_limit_bytes: usize,
    pub(super) cleanup_paths: Vec<PathBuf>,
}

impl ProcessSpec {
    pub(crate) fn new(
        label: &'static str,
        command: impl Into<String>,
        output_limit_bytes: usize,
    ) -> Self {
        Self {
            label,
            command: command.into(),
            args: Vec::new(),
            cwd: None,
            env: BTreeMap::new(),
            stdin: None,
            timeout: None,
            output_limit_bytes,
            cleanup_paths: Vec::new(),
        }
    }

    pub(crate) fn args(mut self, args: Vec<String>) -> Self {
        self.args = args;
        self
    }

    pub(crate) fn cwd(mut self, cwd: impl Into<PathBuf>) -> Self {
        self.cwd = Some(cwd.into());
        self
    }

    pub(crate) fn env(mut self, env: BTreeMap<String, String>) -> Self {
        self.env = env;
        self
    }

    pub(crate) fn stdin(mut self, stdin: Option<ProcessStdin>) -> Self {
        self.stdin = stdin;
        self
    }

    pub(crate) fn timeout(mut self, timeout: Option<Duration>) -> Self {
        self.timeout = timeout;
        self
    }

    #[cfg(feature = "cli-tool")]
    pub(crate) fn cleanup_paths(mut self, cleanup_paths: Vec<PathBuf>) -> Self {
        self.cleanup_paths = cleanup_paths;
        self
    }
}

#[derive(Clone, Debug)]
pub(crate) struct ProcessStdin {
    pub(super) bytes: Vec<u8>,
    pub(super) write_context: &'static str,
}

impl ProcessStdin {
    pub(crate) fn new(bytes: Vec<u8>, write_context: &'static str) -> Self {
        Self {
            bytes,
            write_context,
        }
    }
}

#[derive(Debug)]
pub(crate) struct ProcessOutcome {
    pub(crate) status: ExitStatus,
    pub(crate) timed_out: bool,
    pub(crate) stdout: CapturedOutput,
    pub(crate) stderr: CapturedOutput,
    pub(crate) duration_ms: u64,
    pub(crate) cleanup_errors: Vec<String>,
}

#[derive(Debug, Error)]
pub(crate) enum ProcessSupervisorError {
    #[error("process I/O failed while {context}: {source}")]
    Io {
        context: String,
        #[source]
        source: std::io::Error,
    },
}

impl ProcessSupervisorError {
    pub(crate) fn io(context: impl Into<String>, source: std::io::Error) -> Self {
        Self::Io {
            context: context.into(),
            source,
        }
    }
}
