use anyhow::Context;
use axum::{
    extract::FromRequestParts,
    http::request::Parts,
    response::{IntoResponse, Redirect, Response},
    RequestPartsExt,
};
use diesel::{
    associations::Identifiable,
    deserialize::Queryable,
    dsl::{auto_type, insert_into, AsSelect, Eq, Select},
    pg::Pg,
    prelude::*,
    Selectable,
};
use uuid::Uuid;

use crate::{
    app_error::AppError,
    app_state::AppState,
    auth::AuthInfo,
    schema::{team_memberships, teams, users},
    team_memberships::TeamMembership,
    teams::Team,
};

#[derive(Clone, Debug, Identifiable, Insertable, Queryable, Selectable)]
#[diesel(table_name = users)]
#[diesel(check_for_backend(Pg))]
pub struct User {
    pub id: Uuid,
    pub uid: String,
    pub email: String,
}

impl User {
    pub fn all() -> Select<users::table, AsSelect<User, Pg>> {
        users::table.select(User::as_select())
    }

    pub fn with_uid(uid_value: &str) -> Eq<users::uid, &str> {
        users::uid.eq(uid_value)
    }

    #[auto_type(no_type_alias)]
    pub fn team_memberships(self) -> _ {
        let user_id: Uuid = self.id.clone();
        let user_id_filter: Eq<team_memberships::user_id, Uuid> =
            TeamMembership::with_user_id(user_id);
        let select: AsSelect<(TeamMembership, Team), Pg> = <(TeamMembership, Team)>::as_select();
        team_memberships::table
            .inner_join(teams::table)
            .filter(user_id_filter)
            .select(select)
    }
}

#[derive(Clone, Debug)]
pub struct CurrentUser(pub User);

impl FromRequestParts<AppState> for CurrentUser {
    type Rejection = CurrentUserRejection;

    async fn from_request_parts(
        parts: &mut Parts,
        state: &AppState,
    ) -> Result<Self, <Self as FromRequestParts<AppState>>::Rejection> {
        let auth_info = parts
            .extract_with_state::<AuthInfo, AppState>(state)
            .await
            .map_err(|_| CurrentUserRejection::AuthRequired(state.settings.base_path.clone()))?;
        let current_user = state
            .db_pool
            .get()
            .await
            .map_err(|err| CurrentUserRejection::InternalServerError(err.into()))?
            .interact(move |conn| {
                let maybe_current_user = User::all()
                    .filter(User::with_uid(&auth_info.sub))
                    .first(conn)
                    .optional()
                    .context("failed to load maybe_current_user")?;
                if let Some(current_user) = maybe_current_user {
                    return Ok(current_user);
                }
                let new_user = User {
                    id: Uuid::now_v7(),
                    uid: auth_info.sub.clone(),
                    email: auth_info.email,
                };
                match insert_into(users::table)
                    .values(new_user)
                    .on_conflict(users::uid)
                    .do_nothing()
                    .returning(User::as_returning())
                    .get_result(conn)
                {
                    QueryResult::Err(diesel::result::Error::NotFound) => {
                        tracing::debug!("detected race to insert current user record");
                        User::all()
                            .filter(User::with_uid(&auth_info.sub))
                            .first(conn)
                            .context(
                                "failed to load record after detecting race to insert current user",
                            )
                    }
                    QueryResult::Err(err) => {
                        Err(err).context("failed to insert current user record")
                    }
                    QueryResult::Ok(result) => Ok(result),
                }
            })
            .await
            .unwrap()
            .map_err(|err| CurrentUserRejection::InternalServerError(err.into()))?;
        Ok(CurrentUser(current_user))
    }
}

pub enum CurrentUserRejection {
    AuthRequired(String),
    InternalServerError(AppError),
}

impl IntoResponse for CurrentUserRejection {
    fn into_response(self) -> Response {
        match self {
            Self::AuthRequired(base_path) => {
                Redirect::to(&format!("{}/auth/login", base_path)).into_response()
            }
            Self::InternalServerError(err) => err.into_response(),
        }
    }
}
