use crate::authentication::Authentication;
use crate::data::{AccessTokenResult, PinPollResult, PinPollState, PinRequestResult};
use crate::error::Error;
use async_trait::async_trait;
use std::collections::HashMap;
use std::convert::TryInto;
use std::thread;
use std::time::{Duration, Instant};
use url::Url;

pub struct PinCodeAuthentication {
    access_token: Option<String>,
    valid_to: Option<Instant>,
    refresh_token: Option<String>,
    activate_url: fn(activation_url: &Url),
}

impl PinCodeAuthentication {
    /// Creates a new `PinCodeAuthentication`
    ///
    /// ## Arguments
    ///
    /// * `activate_url` - `PinCodeAuthentication` is an interactive
    /// authentication method. This function will be called with
    /// a URL that needs to be visited by a user in order
    /// to authenticate. When the user has acknowledged by
    /// visiting the URL and logging in, authentication will complete.
    ///
    /// ## Example
    ///
    /// ```
    /// use imagevault::authentication::PinCodeAuthentication;
    /// use url::Url;
    ///
    /// let auth = PinCodeAuthentication::new(activate_url);
    ///
    /// // This is called with a URL the user needs to visit,
    /// // login and acknowledge the client.
    /// fn activate_url(url: &Url) {
    ///     println!("Login to authenticate this client: {}", url);
    /// }
    ///
    /// ```
    ///
    /// ## Remarks
    /// The Pin request for authentication is only valid for a limited
    /// time. If the user has not visited the URL and logged in for
    /// about 5 minutes, the authentication will time out and fail.
    pub fn new(activate_url: fn(activate_url: &Url)) -> Self {
        PinCodeAuthentication {
            access_token: None,
            valid_to: None,
            activate_url,
            refresh_token: None,
        }
    }
}

type BoxedError = Box<dyn std::error::Error>;
const PIN_REQUEST_ENDPOINT: &'static str = "/apiv2/oauth/authorize";
const TOKEN_ENDPOINT: &'static str = "/apiv2/oauth/token";
const PIN_CODE_REDIRECT_ACTIVATE: &'static str = "/Activate";

#[async_trait]
impl Authentication for PinCodeAuthentication {
    async fn authenticate(
        &mut self,
        client_identity: &str,
        client_secret: &str,
        base_url: &Url,
        reqwest_client: &reqwest::Client,
    ) -> Result<String, BoxedError> {
        if let Some(access_token) = &self.access_token {
            // We have a token - is it valid still?
            if let Some(validity) = self.valid_to {
                if validity > Instant::now() {
                    // We are good - return token
                    return Ok(access_token.to_string());
                }
            }
        }
        // Do we have a refresh token?
        if let Some(refresh_token) = &self.refresh_token {
            // try to authenticate with it
            let refresh_token_url = base_url.join(TOKEN_ENDPOINT)?;
            let mut refresh_token_params = HashMap::new();
            refresh_token_params.insert("grant_type", "refresh_token");
            refresh_token_params.insert("refresh_token", refresh_token);
            let refresh_token_result = reqwest_client
                .post(refresh_token_url)
                .basic_auth(client_identity, Some(client_secret))
                .form(&refresh_token_params)
                .send()
                .await;
            // No error with request
            if let Ok(refresh_token_response) = refresh_token_result {
                // No error result
                if let Ok(_) = refresh_token_response.error_for_status_ref() {
                    let json_result = refresh_token_response.json::<AccessTokenResult>().await;
                    if let Ok(val) = json_result {
                        self.access_token = Some(val.access_token.clone());
                        self.refresh_token = val.refresh_token;
                        self.valid_to = Some(
                            Instant::now()
                                + Duration::from_secs(val.expires_in.try_into().unwrap()),
                        );
                        return Ok(val.access_token);
                    }
                }
            }
        }

        // No refresh token or refresh token failed
        // Full authentication
        let pin_request_url = base_url.join(PIN_REQUEST_ENDPOINT)?;
        let mut params = HashMap::new();
        params.insert("response_type", "code");
        params.insert("code_type", "pin");

        let response = reqwest_client
            .get(pin_request_url.clone())
            .basic_auth(client_identity, Some(client_secret))
            .query(&params)
            .send()
            .await?;
        if let Err(err) = response.error_for_status_ref() {
            return Err(Box::new(err));
        }
        let pin_request_response = response.json::<PinRequestResult>().await?;

        // Notify activation url
        (self.activate_url)(&base_url.join(&format!("/activate/{}", pin_request_response.pin))?);
        let pin_expired = Instant::now()
            + Duration::from_secs(pin_request_response.expires_in.try_into().unwrap());
        // Poll until timeout/fail/success
        let poll_result = loop {
            if Instant::now() > pin_expired {
                break Err(Error::PinCodeExpired);
            }
            let mut poll_params = HashMap::new();
            poll_params.insert("response_type", "code");
            poll_params.insert("code_type", "pin");
            poll_params.insert("pin", &pin_request_response.pin);

            let poll_response = reqwest_client
                .get(pin_request_url.clone())
                .basic_auth(client_identity, Some(client_secret))
                .query(&poll_params)
                .send()
                .await?;
            if let Err(err) = poll_response.error_for_status_ref() {
                return Err(Box::new(err));
            }
            let poll_result = poll_response.json::<PinPollResult>().await?;
            match poll_result.state {
                PinPollState::Invalid => break Err(Error::PinCodeInvalid),
                PinPollState::Granted => break Ok(poll_result),
                PinPollState::Tentative => {
                    thread::sleep(Duration::from_millis(500));
                    continue;
                }
            }
        }?;

        // We have an auth code, use it with auth code grant
        let auth_code = poll_result.code.unwrap();
        let auth_redirect_uri = base_url.join(PIN_CODE_REDIRECT_ACTIVATE)?.to_string();
        let mut auth_code_params = HashMap::new();
        auth_code_params.insert("grant_type", "authorization_code");
        auth_code_params.insert("code", &auth_code);
        auth_code_params.insert("redirect_uri", &auth_redirect_uri);
        let auth_code_url = base_url.join(TOKEN_ENDPOINT)?;
        let auth_code_response = reqwest_client
            .post(auth_code_url)
            .basic_auth(client_identity, Some(client_secret))
            .form(&auth_code_params)
            .send()
            .await?;

        if let Err(err) = auth_code_response.error_for_status_ref() {
            return Err(Box::new(err));
        }
        let access_token_result = auth_code_response.json::<AccessTokenResult>().await?;
        self.access_token = Some(access_token_result.access_token.clone());
        self.refresh_token = access_token_result.refresh_token;
        self.valid_to = Some(
            Instant::now()
                + Duration::from_secs(access_token_result.expires_in.try_into().unwrap()),
        );
        Ok(access_token_result.access_token)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::testutil::get_test_data;
    use crate::Client;
    use std::str::FromStr;
    #[test]
    fn pin_code_auth_test() {
        let mut rt = tokio::runtime::Runtime::new().unwrap();
        rt.block_on(async {
            // setup mock HTTP response
            let pin_request_regex_string =
                format!(r"^{}.*response_type=code.*", PIN_REQUEST_ENDPOINT);
            let mock_pin_request =
                mockito::mock("GET", mockito::Matcher::Regex(pin_request_regex_string))
                    .expect(1)
                    .with_status(200)
                    .with_header("content-type", "application/json")
                    .with_body(get_test_data("pin_auth_pin_response"))
                    .create();
            let poll_request_regex_string = format!(r"^{}.*pin=UQHA0T.*", PIN_REQUEST_ENDPOINT);
            let mock_poll_request =
                mockito::mock("GET", mockito::Matcher::Regex(poll_request_regex_string))
                    .expect(1)
                    .with_status(200)
                    .with_header("content-type", "application/json")
                    .with_body(get_test_data("pin_poll_pin_response"))
                    .create();
            let mock_token_request = mockito::mock("POST", TOKEN_ENDPOINT)
                .expect(1)
                .with_status(200)
                .with_header("content-type", "application/json")
                .with_body(get_test_data("auth_token_response"))
                .create();
            let auth = PinCodeAuthentication::new(activate_pin);
            let client = Client::new("client_identity", "client_secret", &mockito::server_url())
                .unwrap()
                .with_authentication(auth);
            client.test_authenticate().await.unwrap();
            mock_pin_request.assert();
            mock_poll_request.assert();
            mock_token_request.assert();
        });
    }

    fn activate_pin(url: &Url) {
        assert!(
            *url == Url::from_str(format!("{}/activate/UQHA0T", &mockito::server_url()).as_ref())
                .unwrap()
        )
    }
}
