use std::{
    convert::AsRef,
    ffi::OsStr,
    fmt::{Debug, Display},
    iter::Iterator,
    process::{Command, ExitStatus, Stdio},
    str::FromStr,
};

use log::{debug, error, trace, warn};
use serde::{Deserialize, Serialize, Serializer};
use serde_with::{DisplayFromStr, PickFirst, serde_as};

use crate::error::{ErrorKind, RusticError, RusticResult};

/// [`CommandInputErrorKind`] describes the errors that can be returned from the `CommandInput`
#[derive(thiserror::Error, Debug, displaydoc::Display)]
#[non_exhaustive]
pub enum CommandInputErrorKind {
    /// Command execution failed: {context}:{what} : {source}
    CommandExecutionFailed {
        /// The context in which the command was called
        context: String,

        /// The action that was performed
        what: String,

        /// The source of the error
        source: std::io::Error,
    },
    /// Command error status: {context}:{what} : {status}
    CommandErrorStatus {
        /// The context in which the command was called
        context: String,

        /// The action that was performed
        what: String,

        /// The exit status of the command
        status: ExitStatus,
    },
    /// Splitting arguments failed: {arguments} : {source}
    SplittingArgumentsFailed {
        /// The arguments that were tried to be split
        arguments: String,

        /// The source of the error
        source: shell_words::ParseError,
    },
    /// Process execution failed: {command:?} : {path:?} : {source}
    ProcessExecutionFailed {
        /// The command that was tried to be executed
        command: CommandInput,

        /// The path in which the command was tried to be executed
        path: std::path::PathBuf,

        /// The source of the error
        source: std::io::Error,
    },
}

pub(crate) type CommandInputResult<T> = Result<T, CommandInputErrorKind>;

/// A command to be called which can be given as CLI option as well as in config files
/// `CommandInput` implements Serialize/Deserialize as well as `FromStr`.
#[serde_as]
#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize)]
pub struct CommandInput(
    // Note: we use _CommandInput here which itself impls FromStr in order to use serde_as PickFirst for CommandInput.
    //#[serde(
    //    serialize_with = "serialize_command",
    //    deserialize_with = "deserialize_command"
    //)]
    #[serde_as(as = "PickFirst<(DisplayFromStr,_)>")] _CommandInput,
);

impl Serialize for CommandInput {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        // if on_failure is default, we serialize to the short `Display` version, else we serialize the struct
        if self.0.on_failure == OnFailure::default() {
            serializer.serialize_str(&self.to_string())
        } else {
            self.0.serialize(serializer)
        }
    }
}

impl From<Vec<String>> for CommandInput {
    fn from(value: Vec<String>) -> Self {
        Self(value.into())
    }
}

impl From<CommandInput> for Vec<String> {
    fn from(value: CommandInput) -> Self {
        value.0.iter().cloned().collect()
    }
}

impl CommandInput {
    /// Returns if a command is set
    #[must_use]
    pub fn is_set(&self) -> bool {
        !self.0.command.is_empty()
    }

    /// Returns the command
    #[must_use]
    pub fn command(&self) -> &str {
        &self.0.command
    }

    /// Returns the command args
    #[must_use]
    pub fn args(&self) -> &[String] {
        &self.0.args
    }

    /// Returns the error handling for the command
    #[must_use]
    pub fn on_failure(&self) -> OnFailure {
        self.0.on_failure
    }

    /// Appends the given arg to the existing list of args for this command
    pub fn append_arg(&mut self, arg: String) {
        self.0.args.push(arg);
    }

    /// Runs the command if it is set
    ///
    /// # Errors
    ///
    /// * If return status cannot be read
    pub fn run<I, K, V>(&self, context: &str, what: &str, env: I) -> RusticResult<()>
    where
        I: IntoIterator<Item = (K, V)>,
        K: AsRef<OsStr>,
        V: AsRef<OsStr>,
    {
        if !self.is_set() {
            trace!("not calling command {context}:{what} - not set");
            return Ok(());
        }
        debug!("calling command {context}:{what}: {self:?}");

        let status = Command::new(self.command())
            .args(self.args())
            .envs(env)
            .status();

        self.on_failure().handle_status(status, context, what)?;
        Ok(())
    }

    /// Runs the command and get its stdout
    ///
    /// # Returns
    /// The stdout as `Vec<u8>`
    ///
    /// # Errors
    ///
    /// * If command cannot be executed
    pub fn stdout(&self) -> RusticResult<Vec<u8>> {
        debug!("commands: {self:?}");
        let run_command = Command::new(self.command())
            .args(self.args())
            .stdout(Stdio::piped())
            .spawn();

        let process = match run_command {
            Ok(process) => process,
            Err(err) => {
                error!("password-command could not be executed: {err}");
                return Err(RusticError::with_source(
                    ErrorKind::Credentials,
                    "Password command `{command}` could not be executed",
                    err,
                )
                .attach_context("command", self.to_string()));
            }
        };

        let output = match process.wait_with_output() {
            Ok(output) => output,
            Err(err) => {
                error!("error reading output from password-command: {err}");
                return Err(RusticError::with_source(
                    ErrorKind::Credentials,
                    "Error reading output from password command `{command}`",
                    err,
                )
                .attach_context("command", self.to_string()));
            }
        };

        if !output.status.success() {
            #[allow(clippy::option_if_let_else)]
            let s = match output.status.code() {
                Some(c) => format!("exited with status code {c}"),
                None => "was terminated".into(),
            };
            error!("password-command {s}");
            return Err(RusticError::new(
                ErrorKind::Credentials,
                "Password command `{command}` did not exit successfully: `{status}`",
            )
            .attach_context("command", self.to_string())
            .attach_context("status", s));
        }

        Ok(output.stdout)
    }

    /// Check if the command uses plural placeholders (%ids or %paths)
    /// Returns true if the command should be executed once with all values,
    /// false if it should be executed once per pack
    pub(crate) fn uses_plural_placeholders(&self) -> RusticResult<bool> {
        let cmd_str = self.to_string();
        let has_id = contains_exact(&cmd_str, "%id");
        let has_ids = contains_exact(&cmd_str, "%ids");
        let has_path = contains_exact(&cmd_str, "%path");
        let has_paths = contains_exact(&cmd_str, "%paths");

        // Check for at least one placeholder
        if !has_id && !has_ids && !has_path && !has_paths {
            return Err(RusticError::new(
            ErrorKind::MissingInput,
            "No placeholder found in warm-up command. Please specify at least one of: %id, %ids, %path, %paths",
        )
        .attach_context("command", cmd_str));
        }

        // Check for mixing singular and plural placeholders
        let has_singular = has_id || has_path;
        let has_plural = has_ids || has_paths;

        if has_singular && has_plural {
            return Err(RusticError::new(
            ErrorKind::InvalidInput,
            "Cannot mix singular (%id, %path) and plural (%ids, %paths) placeholders in warm-up command",
        )
        .attach_context("command", cmd_str));
        }

        Ok(has_plural)
    }
}

impl FromStr for CommandInput {
    type Err = CommandInputErrorKind;
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        Ok(Self(_CommandInput::from_str(s)?))
    }
}

impl Display for CommandInput {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        Display::fmt(&self.0, f)
    }
}

#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default, rename_all = "kebab-case")]
struct _CommandInput {
    command: String,
    args: Vec<String>,
    on_failure: OnFailure,
}

impl _CommandInput {
    fn iter(&self) -> impl Iterator<Item = &String> {
        std::iter::once(&self.command).chain(self.args.iter())
    }
}

impl From<Vec<String>> for _CommandInput {
    fn from(mut value: Vec<String>) -> Self {
        if value.is_empty() {
            Self::default()
        } else {
            let command = value.remove(0);
            Self {
                command,
                args: value,
                ..Default::default()
            }
        }
    }
}

impl FromStr for _CommandInput {
    type Err = CommandInputErrorKind;
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        Ok(split(s)?.into())
    }
}

impl Display for _CommandInput {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let s = shell_words::join(self.iter());
        f.write_str(&s)
    }
}

/// Error handling for commands called as `CommandInput`
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum OnFailure {
    /// errors in command calling will result in rustic errors
    #[default]
    Error,
    /// errors in command calling will result in rustic warnings, but are otherwise ignored
    Warn,
    /// errors in command calling will be ignored
    Ignore,
}

impl OnFailure {
    fn eval<T>(self, res: CommandInputResult<T>) -> RusticResult<Option<T>> {
        let res = self.display_result(res);
        match (res, self) {
            (Err(err), Self::Error) => Err(err),
            (Err(_), _) => Ok(None),
            (Ok(res), _) => Ok(Some(res)),
        }
    }

    /// Displays a result depending on the defined error handling while still yielding the same result
    ///
    /// # Note
    ///
    /// This can be used where an error might occur, but in that
    /// case we have to abort.
    pub fn display_result<T>(self, res: CommandInputResult<T>) -> RusticResult<T> {
        if let Err(err) = &res {
            match self {
                Self::Error => {
                    error!("{err}");
                }
                Self::Warn => {
                    warn!("{err}");
                }
                Self::Ignore => {}
            }
        }

        res.map_err(|err| {
            RusticError::with_source(
                ErrorKind::ExternalCommand,
                "Experienced an error while calling an external command.",
                err,
            )
        })
    }

    /// Handle a status of a called command depending on the defined error handling
    pub fn handle_status(
        self,
        status: Result<ExitStatus, std::io::Error>,
        context: &str,
        what: &str,
    ) -> RusticResult<()> {
        let status = status.map_err(|err| CommandInputErrorKind::CommandExecutionFailed {
            context: context.to_string(),
            what: what.to_string(),
            source: err,
        });

        let Some(status) = self.eval(status)? else {
            return Ok(());
        };

        if !status.success() {
            let _: Option<()> = self.eval(Err(CommandInputErrorKind::CommandErrorStatus {
                context: context.to_string(),
                what: what.to_string(),
                status,
            }))?;
        }
        Ok(())
    }
}

/// helper to split arguments
// TODO: Maybe use special parser (winsplit?) for windows?
fn split(s: &str) -> CommandInputResult<Vec<String>> {
    shell_words::split(s).map_err(|err| CommandInputErrorKind::SplittingArgumentsFailed {
        arguments: s.to_string(),
        source: err,
    })
}

/// Check if the string contains the exact pattern as a standalone token
/// (not as a substring of a longer pattern like "%ids" contains "%id")
fn contains_exact(s: &str, pattern: &str) -> bool {
    let mut start = 0;
    while let Some(pos) = s[start..].find(pattern) {
        let actual_pos = start + pos;
        let pattern_end = actual_pos + pattern.len();

        // Check what character follows the pattern
        let is_word_boundary = pattern_end >= s.len()
            || !s[pattern_end..]
                .chars()
                .next()
                .is_some_and(char::is_alphanumeric);

        if is_word_boundary {
            return true;
        }
        start = pattern_end;
    }
    false
}
