use std::sync::Arc;

use async_trait::async_trait;
use chrono::{DateTime, Duration, FixedOffset, Utc};
use oauth2::{
    AuthorizationCode, PkceCodeVerifier, TokenResponse, basic::BasicTokenResponse,
    url::form_urlencoded::parse,
};
use secrecy::SecretString;
use serde_json::Value;
use shield::{
    ConfigurationError, CreateEmailAddress, CreateUser, Form, MethodAction, MethodSession, Request,
    RequestMethod, Response, ResponseType, SessionAction, ShieldError, SignInCallbackAction,
    UpdateUser, User, erased_method_action,
};

use crate::{
    client::async_http_client,
    connection::{CreateOauthConnection, OauthConnection, UpdateOauthConnection},
    options::OauthOptions,
    provider::{OauthProvider, OauthProviderPkceCodeChallenge},
    session::OauthSession,
    storage::OauthStorage,
};

pub struct OauthSignInCallbackAction<U: User> {
    options: OauthOptions,
    storage: Arc<dyn OauthStorage<U>>,
}

impl<U: User> OauthSignInCallbackAction<U> {
    pub fn new(options: OauthOptions, storage: Arc<dyn OauthStorage<U>>) -> Self {
        Self { options, storage }
    }

    // TODO: Consider if there is a better location for the functions below.

    async fn create_user(&self, email: Option<&str>, name: Option<&str>) -> Result<U, ShieldError> {
        if let Some(email) = email {
            match self.storage.user_by_email(email).await? {
                Some(_) => Err(ShieldError::Validation(
                    "\
                Email address `{email}` is already used by another account. \
                To link a new provider, sign in to with your exising account first. \
                If this is not your account, please contact support for assistence.\
                "
                    .to_owned(),
                )),
                None => Ok(self
                    .storage
                    .create_user(
                        CreateUser {
                            name: name.map(ToOwned::to_owned),
                        },
                        CreateEmailAddress {
                            email: email.to_string(),
                            is_primary: true,
                            // TODO: from claim?
                            is_verified: false,
                            // TODO: generate if not verified
                            verification_token: None,
                            verification_token_expired_at: None,
                            verified_at: None,
                        },
                    )
                    .await?),
            }
        } else {
            Err(ShieldError::Validation(
                "Missing email address in OpenID Connect claims.".to_owned(),
            ))
        }
    }

    async fn update_user(&self, user_id: &str, name: Option<&str>) -> Result<U, ShieldError> {
        self.storage
            .update_user(UpdateUser {
                id: user_id.to_owned(),
                name: name.map(ToOwned::to_owned).map(Some),
            })
            .await
            .map_err(ShieldError::Storage)
    }

    async fn create_oauth_connection(
        &self,
        provider_id: String,
        user_id: String,
        identifier: String,
        token_response: BasicTokenResponse,
    ) -> Result<OauthConnection, ShieldError> {
        let (token_type, access_token, refresh_token, expired_at, scopes) =
            parse_token_response(token_response)?;

        self.storage
            .create_oauth_connection(CreateOauthConnection {
                identifier,
                token_type,
                access_token,
                refresh_token,
                expired_at,
                scopes,
                provider_id,
                user_id,
            })
            .await
            .map_err(ShieldError::Storage)
    }

    async fn update_oauth_connection(
        &self,
        connection_id: String,
        token_response: BasicTokenResponse,
    ) -> Result<OauthConnection, ShieldError> {
        let (token_type, access_token, refresh_token, expired_at, scopes) =
            parse_token_response(token_response)?;

        self.storage
            .update_oauth_connection(UpdateOauthConnection {
                id: connection_id,
                token_type: Some(token_type),
                access_token: Some(access_token),
                refresh_token: refresh_token.map(Some),
                expired_at: expired_at.map(Some),
                scopes: scopes.map(Some),
            })
            .await
            .map_err(ShieldError::Storage)
    }
}

#[async_trait]
impl<U: User + 'static> MethodAction<OauthProvider, OauthSession> for OauthSignInCallbackAction<U> {
    fn id(&self) -> String {
        SignInCallbackAction::id()
    }

    fn name(&self) -> String {
        SignInCallbackAction::name()
    }

    fn openapi_summary(&self) -> &'static str {
        "Sign in callback for OAuth"
    }

    fn openapi_description(&self) -> &'static str {
        "Sign in callback for OAuth."
    }

    fn method(&self) -> RequestMethod {
        RequestMethod::Get
    }

    fn condition(
        &self,
        provider: &OauthProvider,
        session: &MethodSession<OauthSession>,
    ) -> Result<bool, ShieldError> {
        SignInCallbackAction::condition(provider, session)
    }

    async fn forms(&self, _provider: OauthProvider) -> Result<Vec<Form>, ShieldError> {
        Ok(vec![])
    }

    async fn call(
        &self,
        provider: OauthProvider,
        session: &MethodSession<OauthSession>,
        request: Request,
    ) -> Result<Response, ShieldError> {
        let OauthSession {
            csrf,
            pkce_verifier,
            ..
        } = &session.method;

        let state = request
            .query
            .get("state")
            .and_then(|code| code.as_str())
            .ok_or_else(|| ShieldError::Validation("Missing state.".to_owned()))?;

        if csrf.as_ref().is_none_or(|csrf| csrf != state) {
            return Err(ShieldError::Validation("Invalid state.".to_owned()));
        }

        let authorization_code = request
            .query
            .get("code")
            .and_then(|code| code.as_str())
            .ok_or_else(|| ShieldError::Validation("Missing authorization code.".to_owned()))?;

        let client = provider.oauth_client().await?;

        let mut token_request = client
            .exchange_code(AuthorizationCode::new(authorization_code.to_owned()))
            .map_err(|err| {
                ShieldError::Configuration(ConfigurationError::Missing(err.to_string()))
            })?;

        if let Some(pkce_verifier) = pkce_verifier {
            token_request =
                token_request.set_pkce_verifier(PkceCodeVerifier::new(pkce_verifier.to_owned()));
        } else if provider.pkce_code_challenge != OauthProviderPkceCodeChallenge::None {
            return Err(ShieldError::Validation("Missing PKCE verifier.".to_owned()));
        }

        if let Some(token_url_params) = &provider.token_url_params {
            let params = parse(token_url_params.trim_start_matches('?').as_bytes());

            for (name, value) in params {
                token_request =
                    token_request.add_extra_param(name.into_owned(), value.into_owned());
            }
        }

        let async_http_client = async_http_client()?;

        let token_response = token_request
            .request_async(&async_http_client)
            .await
            .map_err(|err| ShieldError::Request(err.to_string()))?;

        let user_response = async_http_client
            .get(&provider.user_url)
            .bearer_auth(token_response.access_token().secret())
            .header("Accept", "application/json")
            .send()
            .await
            .map_err(|err| ShieldError::Request(err.to_string()))?;

        let user = user_response
            .json::<Value>()
            .await
            .map_err(|err| ShieldError::Request(err.to_string()))?;

        let user = if let Some(user_path) = &provider.user_path {
            value_by_path(&user, user_path)?
        } else {
            &user
        };

        let identifier = value_by_path(user, &provider.user_id_path)?;
        let identifier = identifier
            .as_str()
            .map(ToOwned::to_owned)
            .or_else(|| identifier.as_number().map(|number| number.to_string()))
            .ok_or_else(|| ShieldError::Request("Missing or invalid user ID.".to_owned()))?;

        let email = if let Ok(email) = value_by_path(user, &provider.user_email_path) {
            email.as_str()
        } else {
            None
        };

        let name = if let Ok(name) = value_by_path(user, &provider.user_name_path) {
            name.as_str()
        } else {
            None
        };

        let (connection, user) = match self
            .storage
            .oauth_connection_by_identifier(&provider.id, &identifier)
            .await?
        {
            Some(connection) => {
                let connection = self
                    .update_oauth_connection(connection.id, token_response)
                    .await?;

                let user = self.update_user(&connection.user_id, name).await?;

                (connection, user)
            }
            None => {
                let user = self.create_user(email, name).await?;

                let connection = self
                    .create_oauth_connection(
                        provider.id.clone(),
                        user.id(),
                        identifier.to_owned(),
                        token_response,
                    )
                    .await?;

                (connection, user)
            }
        };

        Ok(Response::new(ResponseType::Redirect(
            session
                .method
                .redirect_url
                .as_ref()
                .map(ToString::to_string)
                .unwrap_or_else(|| self.options.sign_in_redirect.clone()),
        ))
        .session_action(SessionAction::authenticate(&provider, user))
        .session_action(SessionAction::method_data(
            &provider,
            OauthSession {
                redirect_url: None,
                csrf: None,
                pkce_verifier: None,
                oauth_connection_id: Some(connection.id),
            },
        )?))
    }
}

erased_method_action!(OauthSignInCallbackAction, <U: User>);

type ParsedTokenResponse = (
    String,
    SecretString,
    Option<SecretString>,
    Option<DateTime<FixedOffset>>,
    Option<Vec<String>>,
);

fn parse_token_response(
    token_response: BasicTokenResponse,
) -> Result<ParsedTokenResponse, ShieldError> {
    Ok((
        token_response.token_type().as_ref().to_string(),
        token_response.access_token().secret().as_str().into(),
        token_response
            .refresh_token()
            .map(|refresh_token| refresh_token.secret().as_str().into()),
        match token_response.expires_in() {
            Some(expires_in) => Some(
                (Utc::now()
                    + Duration::from_std(expires_in)
                        .map_err(|err| ShieldError::Validation(err.to_string()))?)
                .into(),
            ),
            None => None,
        },
        token_response
            .scopes()
            .map(|scopes| scopes.iter().map(|scope| scope.to_string()).collect()),
    ))
}

fn value_by_path<'a>(data: &'a Value, path: &str) -> Result<&'a Value, ShieldError> {
    let mut data = data;

    for key in path.split(".") {
        if let Some(value) = data.get(key) {
            data = value;
        } else {
            return Err(ShieldError::Request(format!(
                "Path `{path}` not found in JSON response."
            )));
        }
    }

    Ok(data)
}
