use std::{str::FromStr, sync::Arc};

use axum::{
    Json, Router,
    extract::{FromRequestParts, Request, State},
    http::StatusCode,
    response::{IntoResponse, Result},
    routing::{get, post},
};
use brevo_mail::{
    brevo::BrevoClient,
    brevo_options::{BrevoEndpoint, BrevoOptions},
    mail_address::MailAddress,
};
use tower_cookies::{Cookie, Cookies};
use tracing::{debug, trace};

use crate::{
    constants::SESSION_COOKIE,
    database::{db::Db, macros::NewId},
    error::CmsError,
    helpers::{create_session_cookie, hash_password, verify_password},
    repositories::auth_repository::AuthRepository,
    types::user::{LoginUser, RegisterUser, RoleValue, SessionId, User, VerificationId},
};

#[derive(Clone, Debug)]
pub struct AuthState {
    auth_repository: AuthRepository,
    brevo_client: Arc<BrevoClient>,
    verify_endpoint: Arc<String>,
}

// pub async fn extend_auth_cookie_lifetime_middleware(
//     jar: CookieJar,
//     State(cookie_options): State<CookieOptions>,
//     request: Request,
//     next: axum::middleware::Next,
// ) -> impl IntoResponse {
//     let response = next.run(request).await;
//
//     let jar = match jar.get(SESSION_COOKIE) {
//         Some(session_cookie) => {
//             let session_id = session_cookie.value().to_owned();
//             let cookie = reset_session_cookie(session_cookie.clone(), cookie_options);
//             let jar = jar.add(cookie);
//
//             jar
//         }
//         None => jar,
//     };
//
//     (jar, response)
// }

impl FromRequestParts<Arc<AuthRepository>> for User {
    type Rejection = CmsError;

    async fn from_request_parts(
        parts: &mut axum::http::request::Parts,
        state: &Arc<AuthRepository>,
    ) -> std::result::Result<Self, Self::Rejection> {
        // is infallible
        let cookies = Cookies::from_request_parts(parts, state).await.unwrap();
        let session_cookie = cookies.get(SESSION_COOKIE);
        match session_cookie {
            Some(session_cookie) => {
                trace!("Session Cookie found");
                let session_id = session_cookie.value();
                let session_id = SessionId::from_str(session_id)?;
                match state.get_user_by_session_id(&session_id) {
                    Ok(user) => {
                        trace!("Found user for session");
                        Ok(user)
                    }
                    Err(_) => Err(CmsError::SessionCookieNotValid(cookies)),
                }
            }
            None => Err(CmsError::SessionCookieNotSet),
        }
    }
}

pub async fn admin_only(
    user: User,
    request: Request,
    next: axum::middleware::Next,
) -> impl IntoResponse {
    if user.role == RoleValue::Admin {
        next.run(request).await
    } else {
        StatusCode::FORBIDDEN.into_response()
    }
}

pub fn auth_app(db: Db, verify_endpoint: String) -> Result<Router, CmsError> {
    tracing::debug!("Create Auth App State and Router");

    let brevo_api_key = db.get_brevo_key()?;
    let brevo_options = BrevoOptions::new(brevo_api_key, BrevoEndpoint::default());
    let brevo_client =
        BrevoClient::from_options(brevo_options).expect("Was not able to create a brevo client");

    let brevo_client = Arc::new(brevo_client);
    let auth_repository = AuthRepository { db };
    let verify_endpoint = Arc::new(verify_endpoint.to_owned());
    let auth_state = AuthState {
        auth_repository: auth_repository.clone(),
        brevo_client,
        verify_endpoint,
    };

    // let admin_only_layer = from_fn(admin_only);
    let layer_state = Arc::new(auth_repository.clone());

    let router = Router::new()
        .route("/cms-api/user/register", post(register))
        .route("/cms-api/user/verify", post(verify))
        .route("/cms-api/user/login", post(login))
        .route("/cms-api/user/logout", post(logout))
        .with_state(auth_state)
        .route("/cms-api/user/user", get(get_user))
        .with_state(layer_state);

    Ok(router)
}

pub async fn register(
    State(state): State<AuthState>,
    Json(register_user): Json<RegisterUser>,
) -> Result<impl IntoResponse, CmsError> {
    trace!("Generate Password Hash");
    let hash = hash_password(&register_user.password)?;

    trace!("Register User in DB");
    debug!("just to check");
    let verification_id = state
        .auth_repository
        .register_user(&register_user.email, &hash)?;

    trace!("{:?}", verification_id);

    let verification_link = format!("{}{}", state.verify_endpoint, verification_id.id());
    let message = format!(
        r#"
        <h1>Account Verification</h1>

        <div>Please click the link below to verify your account.</div>
        <a href="{}">{}</a>

        <div>Or enter the verification code yourself.</div>
        <div>{}</div>

        "#,
        verification_link, verification_link, verification_id
    );

    let sender = MailAddress::builder()
        .name("No Reply")
        .mail("info@firewave.io")
        .build()?;

    let to = MailAddress::builder()
        .mail(register_user.email.to_string())
        .build()?;

    let mail = brevo_mail::mail::Mail::builder()
        .subject("Account Verification")
        .content(brevo_mail::mail::MailContent::Html(message))
        .sender(sender)
        .to(vec![to])
        .build()?;

    debug!("Send Verification Mail");
    state.brevo_client.send(&mail).await?;

    Ok(())
}

pub async fn verify(
    State(state): State<AuthState>,
    Json(verification_id): Json<VerificationId>,
) -> Result<()> {
    state.auth_repository.verify_user(verification_id)?;

    Ok(())
}

pub async fn login(
    cookies: Cookies,
    State(state): State<AuthState>,
    Json(login_user): Json<LoginUser>,
) -> Result<Json<User>, CmsError> {
    let hash = state.auth_repository.get_hash_by_email(&login_user.email)?;

    verify_password(login_user.password, hash).await?;

    let user = state
        .auth_repository
        .get_verified_user_by_email(&login_user.email)?;

    trace!("Got verified user: {:?}", user);

    let session_id = state
        .auth_repository
        .create_session_for_user(&user.user_id)?;

    trace!("Created session with id: {:?}", session_id);

    let cookie_options = state.auth_repository.db.get_cookie_options()?;
    let cookie = create_session_cookie(session_id.to_string(), cookie_options);

    cookies.add(cookie);

    Ok(Json(user))
}

async fn logout(cookies: Cookies) -> impl IntoResponse {
    trace!("Logout User. Remove Cookie");

    if cookies.get(SESSION_COOKIE).is_some() {
        let mut cookie = Cookie::new(SESSION_COOKIE, "");
        cookie.set_path("/");

        cookies.remove(cookie);
        trace!("Removed Session Cookie");
    }
}

pub async fn get_user(user: User) -> Result<Json<User>, CmsError> {
    Ok(Json(user))
}
