/* Copyright 2021 Dominik George <nik@naturalnet.de>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

use crate::config::{
    argv_to_config,
    get_config
};
use config::Config;

use crate::logging::setup_log;

use oauth2::{
    AuthUrl,
    ClientId,
    ClientSecret,
    RequestTokenError,
    ResourceOwnerPassword,
    ResourceOwnerUsername,
    Scope,
    TokenUrl
};
use oauth2::basic::{
    BasicClient,
    BasicTokenResponse
};
use oauth2::reqwest;
use oauth2::reqwest::http_client;

use pamsm::{PamServiceModule, Pam, PamFlag, PamError, PamLibExt};

fn get_or_pam_error(config: &Config, key: &str) -> Result<String, PamError> {
    match config.get_str(key) {
        Ok(v) => {
            debug!("Configuration key found: {} = {}", key, v);
            return Ok(v);
        },
        _ => {
            error!("Configuration key not found: {}", key);
            return Err(PamError::SERVICE_ERR);
        },
    }
}

fn do_legacy_auth(username: String, password: String, config: Config) -> Result<BasicTokenResponse, PamError> {
    let client_id = ClientId::new(get_or_pam_error(&config, "pam.client_id")?);
    let client_secret = ClientSecret::new(get_or_pam_error(&config, "pam.client_secret")?);
    let auth_url = match AuthUrl::new(get_or_pam_error(&config, "pam.auth_url")?) {
        Ok(u) => u,
        _ => {
            error!("Could not parse authorization URL");
            return Err(PamError::SERVICE_ERR);
        },
    };
    let token_url = match TokenUrl::new(get_or_pam_error(&config, "pam.token_url")?){
        Ok(u) => u,
        _ => {
            error!("Could not parse token URL");
            return Err(PamError::SERVICE_ERR);
        },
    };
    let scope = get_or_pam_error(&config, "pam.scope")?;

    let client = BasicClient::new(client_id, Some(client_secret), auth_url, Some(token_url));
    let result = client
        .exchange_password(
            &ResourceOwnerUsername::new(username),
            &ResourceOwnerPassword::new(password)
        )
        .add_scope(Scope::new(scope.to_string()))
        .request(http_client);
    match result {
            Ok(t) => Ok(t),
            Err(e) => match e {
                RequestTokenError::Request(re) => match re {
                    reqwest::Error::Reqwest(ree) => {
                        if ree.is_status() {
                            error!("Authentication failed for invalid grant: {}", ree);
                            return Err(PamError::AUTH_ERR);
                        } else {
                            error!("Request error requesting token: {}", ree);
                            return Err(PamError::AUTHINFO_UNAVAIL);
                        }
                    },
                    reqwest::Error::Http(he) => {
                        error!("HTTP error requesting token: {}", he);
                        return Err(PamError::AUTHINFO_UNAVAIL);
                    },
                    reqwest::Error::Io(ioe) => {
                        error!("IO error requesting token: {}", ioe);
                        return Err(PamError::SYSTEM_ERR);
                    },
                    _ => {
                        error!("Unknown error: {}", re);
                        return Err(PamError::SERVICE_ERR);
                    },
                },
                RequestTokenError::ServerResponse(t) => {
                    error!("Authorization server returned error: {}", t);
                    return Err(PamError::AUTH_ERR);
                },
                RequestTokenError::Parse(pe, _) => {
                    error!("Error parsing response: {}", pe);
                    return Err(PamError::SERVICE_ERR);
                },
                _ => {
                    error!("Unknown error: {}", e);
                    return Err(PamError::SERVICE_ERR);
                },
            },
        }
}

fn pam_sm_prepare(argv: &Vec<String>) -> Config {
    let conf_args = argv_to_config(argv);
    let conf = get_config(conf_args);

    let mut log_level = log::LevelFilter::Error;
    if conf.get_bool("debug").unwrap_or_default() || conf.get_bool("pam.debug").unwrap_or_default() {
        log_level = log::LevelFilter::Debug;
    }
    setup_log(log_level);

    return conf;
}

struct PamOidc;

impl PamServiceModule for PamOidc {
    fn authenticate(pamh: Pam, _: PamFlag, argv: Vec<String>) -> PamError {
        let conf = pam_sm_prepare(&argv);
        if conf.get_str("pam.flow").unwrap() == "password" {
            debug!("Starting Resource Owner Password Credentials OAuth flow");

            let username = match pamh.get_user(None) {
                Ok(Some(u)) => match u.to_str() {
                    Ok(u) => u,
                    _ => {
                        error!("Could not convert user name to string, aborting");
                        return PamError::CRED_INSUFFICIENT;
                    },
                },
                Ok(None) => {
                    error!("No username supplied");
                    return PamError::CRED_INSUFFICIENT;
                },
                Err(e) => {
                    error!("Error getting user name: {}", e);
                    return e;
                },
            };
            debug!("Successfully got user name");

            let password = match pamh.get_authtok(None) {
                Ok(Some(p)) => match p.to_str() {
                    Ok(p) => p,
                    _ => {
                        error!("Could not convert password to string");
                        return PamError::CRED_INSUFFICIENT;
                    },
                },
                Ok(None) => {
                    error!("No password supplied");
                    return PamError::CRED_INSUFFICIENT;
                },
                Err(e) => {
                    error!("Error getting password: {}", e);
                    return e;
                },
            };
            debug!("Successfully got password");

            match do_legacy_auth(username.to_string(), password.to_string(), conf) {
                Ok(_) => {
                    info!("Authenticated {} using Resource Owner Password Grant", username);
                    return PamError::SUCCESS;
                },
                Err(e) => {
                    error!("Error in legacy authentication: {}", e);
                    return e;
                },
            };
        }

        error!("Unknown flow for authentication");
        return PamError::SERVICE_ERR;
    }
}

pam_module!(PamOidc);