use std::{
    process::{Command, Stdio},
    str::FromStr,
};

use crate::prelude::*;
use itertools::Itertools;
use news_flash::models::{
    ApiSecret, BasicAuth, DirectLogin, LoginData, OAuthData, PasswordLogin, PluginID, TokenLogin,
};

#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
pub struct LoginConfiguration {
    pub login_type: LoginType,
    pub provider: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub user: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub url: Option<String>,

    #[serde(skip_serializing_if = "Option::is_none")]
    pub password: Option<Secret>,

    #[serde(skip_serializing_if = "Option::is_none")]
    pub token: Option<Secret>,

    #[serde(skip_serializing_if = "Option::is_none")]
    pub oauth_client_id: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub oauth_client_secret: Option<Secret>,

    #[serde(skip_serializing_if = "Option::is_none")]
    pub basic_auth_user: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub basic_auth_password: Option<Secret>,
}

#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum LoginType {
    NoLogin,
    DirectPassword,
    DirectToken,
    OAuth,
}

#[derive(Clone, Debug)]
pub enum Secret {
    Verbatim(String),
    Command(Vec<String>),
}

impl FromStr for Secret {
    type Err = ConfigError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        Ok(if s.trim().starts_with("cmd:") {
            let (_, command) = s
                .trim()
                .split_once(":")
                .ok_or(ConfigError::SecretParseError)?;
            Secret::Command(shell_words::split(command).map_err(|_| ConfigError::SecretParseError)?)
        } else {
            Secret::Verbatim(s.to_owned())
        })
    }
}

impl Secret {
    pub fn get_secret(&self) -> Result<String, ConfigError> {
        Ok(match self {
            Secret::Verbatim(secret) => secret.clone(),
            Secret::Command(args) => {
                let Some((cmd, args)) = args.split_first() else {
                    return Err(ConfigError::SecretCommandExecutionError(
                        "pass command is empty".to_owned(),
                    ));
                };
                let child = Command::new(cmd)
                    .stdin(Stdio::null())
                    .stdout(Stdio::piped())
                    .stderr(Stdio::piped())
                    .args(args)
                    .spawn()
                    .map_err(|err| ConfigError::SecretCommandExecutionError(err.to_string()))?;

                let output = child
                    .wait_with_output()
                    .map_err(|err| ConfigError::SecretCommandExecutionError(err.to_string()))?;

                if !output.status.success() {
                    return Err(ConfigError::SecretCommandExecutionError(
                        String::from_utf8(output.stderr).map_err(|_| {
                            ConfigError::SecretCommandExecutionError(
                                "cannot read stderr from password command".to_owned(),
                            )
                        })?,
                    ));
                }

                let pass = String::from_utf8(output.stdout).map_err(|_| {
                    ConfigError::SecretCommandExecutionError(
                        "cannot read stdin from password command".to_owned(),
                    )
                })?;

                // trim trailing news lines
                pass.trim_end_matches(['\r', '\n']).to_owned()
            }
        })
    }

    pub fn get_secret_option(secret: Option<&Self>) -> Result<Option<String>, ConfigError> {
        secret.map(Self::get_secret).transpose()
    }
}

impl<'de> serde::de::Deserialize<'de> for Secret {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        Secret::from_str(&String::deserialize(deserializer)?)
            .map_err(|e| serde::de::Error::custom(e.to_string()))
    }
}

impl serde::ser::Serialize for Secret {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        match self {
            Secret::Verbatim(secret) => serializer.serialize_str(secret),
            Secret::Command(args) => {
                let cmd = args.first().unwrap_or(&"".to_owned()).to_owned();

                let args_str = args
                    .iter()
                    .skip(1)
                    .map(|arg| shell_words::quote(arg))
                    .join(" ");

                serializer.serialize_str(&format!("{cmd} {args_str}"))
            }
        }
    }
}

fn unwrap_or_config_error<T>(val: Option<&T>, err: &str) -> Result<T, ConfigError>
where
    T: Clone,
{
    Ok(val
        .ok_or(ConfigError::LoginConfigurationInvalid(err.to_owned()))?
        .to_owned())
}

impl LoginConfiguration {
    pub fn to_login_data(&self) -> Result<LoginData, ConfigError> {
        Ok(match self.login_type {
            LoginType::NoLogin => LoginData::None(PluginID::new(&self.provider)),
            LoginType::DirectPassword => self.to_direct_login()?,
            LoginType::DirectToken => self.to_direct_token()?,
            LoginType::OAuth => self.to_oauth()?,
        })
    }

    fn to_direct_login(&self) -> Result<LoginData, ConfigError> {
        let password = unwrap_or_config_error(
            Secret::get_secret_option(self.password.as_ref())?.as_ref(),
            "password needed",
        )?;

        Ok(LoginData::Direct(DirectLogin::Password(
            news_flash::models::PasswordLogin {
                id: PluginID::new(&self.provider),
                url: self.url.to_owned(),
                user: unwrap_or_config_error(self.user.as_ref(), "user name needed")?,
                password,
                basic_auth: self.to_basic_auth()?,
            },
        )))
    }

    fn to_direct_token(&self) -> Result<LoginData, ConfigError> {
        let token = unwrap_or_config_error(
            Secret::get_secret_option(self.token.as_ref())?.as_ref(),
            "token needed",
        )?;

        Ok(LoginData::Direct(DirectLogin::Token(
            news_flash::models::TokenLogin {
                id: PluginID::new(&self.provider),
                url: self.url.to_owned(),
                token,
                basic_auth: self.to_basic_auth()?,
            },
        )))
    }

    fn to_oauth(&self) -> Result<LoginData, ConfigError> {
        let api_secret = match (
            self.oauth_client_id.as_ref(),
            self.oauth_client_secret.as_ref(),
        ) {
            (None, None) => None,
            (Some(client_id), Some(client_secret)) => Some(ApiSecret {
                client_id: client_id.to_owned(),
                client_secret: client_secret.get_secret()?,
            }),

            _ => {
                return Err(ConfigError::LoginConfigurationInvalid(
                    "either both, oauth_client_id and oauth_client_secret, must be defined or none"
                        .to_owned(),
                ));
            }
        };

        Ok(LoginData::OAuth(OAuthData {
            id: PluginID::new(&self.provider),
            url: unwrap_or_config_error(self.url.as_ref(), "url needed")?,
            custom_api_secret: api_secret,
        }))
    }

    fn to_basic_auth(&self) -> Result<Option<BasicAuth>, ConfigError> {
        let password = Secret::get_secret_option(self.basic_auth_password.as_ref())?;

        Ok(self.basic_auth_user.as_ref().map(|user| BasicAuth {
            user: user.to_owned(),
            password,
        }))
    }

    fn from_none(plugin_id: PluginID) -> Self {
        Self {
            login_type: LoginType::NoLogin,
            provider: plugin_id.as_str().to_owned(),
            ..Default::default()
        }
    }

    fn from_direct_password(direct_login: &PasswordLogin) -> Self {
        Self {
            login_type: LoginType::DirectPassword,
            provider: direct_login.id.as_str().to_owned(),
            url: direct_login.url.to_owned(),
            user: Some(direct_login.user.to_owned()),
            password: Some(Secret::Verbatim(direct_login.password.to_owned())),
            ..Default::default()
        }
        .with_basic_auth(direct_login.basic_auth.as_ref())
    }

    fn with_basic_auth(self, basic_auth: Option<&BasicAuth>) -> Self {
        Self {
            basic_auth_user: basic_auth
                .map(|basic_auth| basic_auth.user.to_owned())
                .clone(),
            basic_auth_password: basic_auth
                .and_then(|basic_auth| basic_auth.password.to_owned())
                .map(Secret::Verbatim),
            ..self
        }
    }

    fn from_direct_token(token_login: &TokenLogin) -> Self {
        Self {
            login_type: LoginType::DirectToken,
            provider: token_login.id.as_str().to_owned(),
            url: token_login.url.to_owned(),
            token: Some(Secret::Verbatim(token_login.token.to_owned())),
            ..Self::default()
        }
        .with_basic_auth(token_login.basic_auth.as_ref())
    }

    fn from_oauth(oauth_data: &OAuthData) -> Self {
        Self {
            login_type: LoginType::OAuth,
            provider: oauth_data.id.as_str().to_owned(),
            url: Some(oauth_data.url.to_owned()),
            oauth_client_id: oauth_data
                .custom_api_secret
                .as_ref()
                .map(|api_secret| api_secret.client_id.to_owned()),
            oauth_client_secret: oauth_data
                .custom_api_secret
                .as_ref()
                .map(|api_secret| Secret::Verbatim(api_secret.client_secret.to_owned())),

            ..Self::default()
        }
    }

    fn redact(pass: Option<Secret>) -> Option<Secret> {
        pass.map(|_| Secret::Verbatim("*******".to_owned()))
    }

    fn redact_secrets(self) -> Self {
        Self {
            password: Self::redact(self.password),
            oauth_client_secret: Self::redact(self.oauth_client_secret),
            basic_auth_password: Self::redact(self.basic_auth_password),
            token: Self::redact(self.token),
            ..self
        }
    }

    pub fn as_toml(&self, show_secrets: bool) -> Result<String, ConfigError> {
        if show_secrets {
            toml::ser::to_string(self)
                .map_err(|err| ConfigError::LoginConfigurationInvalid(err.to_string()))
        } else {
            toml::ser::to_string(&Self::redact_secrets(self.clone()))
                .map_err(|err| ConfigError::LoginConfigurationInvalid(err.to_string()))
        }
    }
}

impl Default for LoginConfiguration {
    fn default() -> Self {
        Self {
            login_type: LoginType::NoLogin,
            provider: "local_rss".to_owned(),
            user: None,
            url: None,

            password: None,

            token: None,

            oauth_client_id: None,
            oauth_client_secret: None,

            basic_auth_user: None,
            basic_auth_password: None,
        }
    }
}

impl From<LoginData> for LoginConfiguration {
    fn from(login_data: LoginData) -> Self {
        match login_data {
            LoginData::None(plugin_id) => Self::from_none(plugin_id),
            LoginData::Direct(DirectLogin::Password(direct_password)) => {
                Self::from_direct_password(&direct_password)
            }
            LoginData::Direct(DirectLogin::Token(direct_token)) => {
                Self::from_direct_token(&direct_token)
            }
            LoginData::OAuth(oauth_data) => Self::from_oauth(&oauth_data),
        }
    }
}

#[cfg(test)]
mod test {
    use super::*;
    use claims::assert_matches;
    use rstest::rstest;

    #[rstest]
    #[case("secret12345")]
    #[case("  secret12345")]
    #[case("secret12345  ")]
    #[case("")]
    fn test_secret_parsing_password(#[case] s: &str) {
        use Secret as P;
        let Ok(P::Verbatim(secret)) = Secret::from_str(s) else {
            panic!("expected Password");
        };

        assert_eq!(secret, s);
    }

    #[rstest]
    #[case("cmd:pass private/eilmeldung", vec!["pass", "private/eilmeldung"])]
    #[case("cmd:/home/user/pass.sh", vec!["/home/user/pass.sh"])]
    #[case(" cmd:   pass  ", vec!["pass"])]
    #[case("cmd:", vec![])]
    fn test_secret_parsing_command(#[case] s: &str, #[case] args: Vec<&str>) {
        use Secret as P;
        let Ok(P::Command(command)) = Secret::from_str(s) else {
            panic!("expected Command");
        };

        assert_eq!(command, args);
    }

    #[rstest]
    #[case("cmd:pass \"private/eilmeldung")]
    #[case("cmd:/home/user/pass.sh\'")]
    fn test_secret_parsing_command_fail(#[case] s: &str) {
        assert_matches!(Secret::from_str(s), Err(ConfigError::SecretParseError));
    }

    #[test]
    fn test_no_login() {
        let login_config = LoginConfiguration {
            login_type: LoginType::NoLogin,
            provider: "abc".to_owned(),
            user: None,
            url: None,
            password: None,
            token: None,

            oauth_client_id: None,
            oauth_client_secret: None,

            basic_auth_user: None,
            basic_auth_password: None,
        };

        let login_data = login_config.to_login_data();

        let Ok(LoginData::None(provider)) = login_data else {
            panic!("expected LoginData::None")
        };

        assert_eq!(provider.as_str(), "abc");
    }

    #[rstest]
    #[case("cmd:echo \"secret\"", "secret")]
    #[case("cmd:  echo \"secret\"", "secret")]
    #[case("cmd:echo 'secret'", "secret")]
    fn test_secret_execute_command(#[case] cmd: &str, #[case] pass: &str) {
        let secret = Secret::from_str(cmd).unwrap();
        let pass_value = secret.get_secret();
        assert_matches!(pass_value, Ok(..));
        assert_eq!(pass_value.unwrap(), pass);
    }

    #[test]
    fn test_secret_execute_command_fail() {
        let secret = Secret::from_str("cmd:exit 1").unwrap();
        let pass_value = secret.get_secret();
        assert_matches!(
            pass_value,
            Err(ConfigError::SecretCommandExecutionError(..))
        );
        let ConfigError::SecretCommandExecutionError(_message) = pass_value.unwrap_err() else {
            panic!("expected SecretCommandExecutionError with error message");
        };
    }

    #[test]
    fn test_direct_login() {
        let login_configuration = LoginConfiguration {
            login_type: LoginType::DirectPassword,
            provider: "abc".to_owned(),
            user: Some("username".to_owned()),
            url: Some("http://www.rssprovider.com/".to_owned()),
            password: Some(Secret::from_str("cmd:echo \"secret\"").unwrap()),
            token: None,

            oauth_client_id: None,
            oauth_client_secret: None,

            basic_auth_user: Some("username".to_owned()),
            basic_auth_password: Some(Secret::from_str("cmd:echo \"secret\"").unwrap()),
        };
        let login_data_res = login_configuration.to_login_data();

        let Ok(LoginData::Direct(DirectLogin::Password(direct_login_data))) = login_data_res else {
            panic!("expected direct login data");
        };

        assert_eq!(direct_login_data.user, "username");
        assert_eq!(direct_login_data.password, "secret");
        assert_eq!(
            direct_login_data.url.unwrap(),
            "http://www.rssprovider.com/"
        );

        let Some(BasicAuth { user, password }) = direct_login_data.basic_auth else {
            panic!("expected basic auth data");
        };

        assert_eq!(user, "username");
        assert_eq!(password.unwrap(), "secret");
    }

    #[test]
    fn test_oauth() {
        let login_configuration = LoginConfiguration {
            login_type: LoginType::OAuth,
            provider: "abc".to_owned(),
            user: None,
            url: Some("http://www.rssprovider.com/".to_owned()),
            password: None,
            token: None,

            oauth_client_id: Some("someclientid".to_owned()),
            oauth_client_secret: Some(Secret::from_str("cmd:echo \"secret\"").unwrap()),

            basic_auth_user: Some("username".to_owned()),
            basic_auth_password: Some(Secret::from_str("cmd:echo \"secret\"").unwrap()),
        };
        let login_data_res = login_configuration.to_login_data();

        assert_matches!(login_data_res, Ok(LoginData::OAuth(..)));

        let Ok(LoginData::OAuth(oauth_data)) = login_data_res else {
            panic!("expected oauth login data");
        };

        assert_eq!(
            oauth_data.custom_api_secret.as_ref().unwrap().client_id,
            "someclientid"
        );
        assert_eq!(
            oauth_data.custom_api_secret.as_ref().unwrap().client_secret,
            "secret"
        );
        assert_eq!(oauth_data.url, "http://www.rssprovider.com/");
    }

    #[rstest]
    #[allow(clippy::too_many_arguments)]    
    #[rustfmt::skip]
    #[case(LoginType::DirectPassword, Some(None), None,       None,       None,       None,       None)]
    #[case(LoginType::DirectPassword, None,       None,       Some(None), None,       None,       None)]
    #[case(LoginType::DirectToken,    None,       None,       None,       Some(None), None,       None)]
    #[case(LoginType::OAuth,          None,       Some(None), None,       None,       None,       None)]
    #[case(LoginType::OAuth,          None,       None,       None,       None,       Some(None), None)]
    #[case(LoginType::OAuth,          None,       None,       None,       None,       None,       Some(None))]
    fn to_login_data_fails(
        #[case] login_type: LoginType,
        #[case] user: Option<Option<&str>>,
        #[case] url: Option<Option<&str>>,
        #[case] password: Option<Option<&str>>,
        #[case] token: Option<Option<&str>>,
        #[case] oauth_client_id: Option<Option<&str>>,
        #[case] oauth_client_secret: Option<Option<&str>>,
    ) {
        let login_configuration = LoginConfiguration {
            login_type,
            provider: "abc".to_owned(),
            user: user.unwrap_or(Some("username")).map(str::to_owned),
            url: url
                .unwrap_or(Some("http://www.rssprovider.com/"))
                .map(str::to_owned),
            password: password
                .unwrap_or(Some("cmd:echo \"secret1\""))
                .map(|cmd| Secret::from_str(cmd).unwrap()),
            token: token
                .unwrap_or(Some("cmd:echo \"secret2\""))
                .map(|cmd| Secret::from_str(cmd).unwrap()),

            oauth_client_id: oauth_client_id
                .unwrap_or(Some("someclientid"))
                .map(str::to_owned),

            oauth_client_secret: oauth_client_secret
                .unwrap_or(Some("cmd:echo \"secret3\""))
                .map(|cmd| Secret::from_str(cmd).unwrap()),

            basic_auth_user: Some("username".to_owned()),
            basic_auth_password: Some(Secret::from_str("cmd:echo \"secret4\"").unwrap()),
        };
        let login_data_res = login_configuration.to_login_data();

        assert_matches!(
            login_data_res,
            Err(ConfigError::LoginConfigurationInvalid(_))
        );
    }
}
