use crate::app::AppState;
use crate::auth::AuthCheck;
use crate::controllers::helpers::Paginate;
use crate::controllers::helpers::pagination::{Paginated, PaginationOptions};
use crate::models::krate::CrateName;
use crate::models::{CrateOwner, Follow, OwnerKind, PublicUser, User, Version, VersionOwnerAction};
use crate::schema::{crate_owners, crates, emails, follows, oauth_github, users, versions};
use crate::util::errors::AppResult;
use crate::util::no_store;
use crate::views::{EncodableMe, EncodablePrivateUser, EncodableVersion, OwnedCrate};
use axum::Json;
use axum_extra::TypedHeader;
use axum_extra::headers::CacheControl;
use diesel::prelude::*;
use diesel_async::{AsyncPgConnection, RunQueryDsl};
use futures_util::FutureExt;
use http::request::Parts;
use serde::Serialize;

/// Get the currently authenticated user.
#[utoipa::path(
    get,
    path = "/api/v1/me",
    security(("cookie" = [])),
    tag = "users",
    extensions(("x-internal" = json!(true))),
    responses(
        (status = 200, description = "Successful Response", body = inline(EncodableMe)),
        (status = "4XX", description = "Client Error", body = crate::util::errors::ApiErrorResponse<'_>),
        (status = "5XX", description = "Server Error", body = crate::util::errors::ApiErrorResponse<'_>),
    ),
)]
pub async fn get_authenticated_user(
    app: AppState,
    req: Parts,
) -> AppResult<(TypedHeader<CacheControl>, Json<EncodableMe>)> {
    let mut conn = app.db_read_prefer_primary().await?;
    let user_id = AuthCheck::only_cookie()
        .check(&req, &mut conn)
        .await?
        .user_id();

    Ok((no_store(), authenticated_user(&mut conn, user_id).await?))
}

/// Loads the profile of the user with the given id.
pub async fn authenticated_user(
    conn: &mut AsyncPgConnection,
    user_id: i32,
) -> AppResult<Json<EncodableMe>> {
    let ((user, verified, email, verification_sent), owned_crates) = tokio::try_join!(
        users::table
            .find(user_id)
            .left_join(emails::table)
            .left_join(oauth_github::table)
            .select((
                User::as_select(),
                emails::verified.nullable(),
                emails::email.nullable(),
                emails::token_generated_at.nullable().is_not_null(),
            ))
            .first::<(User, Option<bool>, Option<String>, bool)>(&mut &*conn)
            .boxed(),
        CrateOwner::by_owner_kind(OwnerKind::User)
            .inner_join(crates::table)
            .filter(crate_owners::owner_id.eq(user_id))
            .select((crates::id, crates::name, crate_owners::email_notifications))
            .order(crates::name.asc())
            .load(&mut &*conn)
            .boxed()
    )?;

    let owned_crates = owned_crates
        .into_iter()
        .map(|(id, name, email_notifications)| OwnedCrate {
            id,
            name,
            email_notifications,
        })
        .collect();

    let verified = verified.unwrap_or(false);
    let verification_sent = verified || verification_sent;
    Ok(Json(EncodableMe {
        user: EncodablePrivateUser::from(user, email, verified, verification_sent),
        owned_crates,
    }))
}

#[derive(Debug, Serialize, utoipa::ToSchema)]
pub struct UpdatesResponse {
    /// The list of recent versions of crates that the authenticated user follows.
    pub versions: Vec<EncodableVersion>,

    #[schema(inline)]
    pub meta: UpdatesResponseMeta,
}

#[derive(Debug, Serialize, utoipa::ToSchema)]
pub struct UpdatesResponseMeta {
    /// Whether there are more versions to be loaded.
    pub more: bool,
}

/// List versions of crates that the authenticated user follows.
#[utoipa::path(
    get,
    path = "/api/v1/me/updates",
    security(("cookie" = [])),
    tag = "versions",
    extensions(("x-internal" = json!(true))),
    responses(
        (status = 200, description = "Successful Response", body = inline(UpdatesResponse)),
        (status = "4XX", description = "Client Error", body = crate::util::errors::ApiErrorResponse<'_>),
        (status = "5XX", description = "Server Error", body = crate::util::errors::ApiErrorResponse<'_>),
    ),
)]
pub async fn get_authenticated_user_updates(
    app: AppState,
    req: Parts,
) -> AppResult<(TypedHeader<CacheControl>, Json<UpdatesResponse>)> {
    let mut conn = app.db_read_prefer_primary().await?;
    let auth = AuthCheck::only_cookie().check(&req, &mut conn).await?;

    let user = auth.user();

    let followed_crates = Follow::belonging_to(user).select(follows::crate_id);
    let query = versions::table
        .inner_join(crates::table)
        .left_outer_join(users::table.left_join(oauth_github::table))
        .filter(crates::id.eq_any(followed_crates))
        .order(versions::created_at.desc())
        .select(<(Version, CrateName, Option<PublicUser>)>::as_select())
        .pages_pagination(PaginationOptions::builder().gather(&req)?);

    let data: Paginated<(Version, CrateName, Option<PublicUser>)> = query.load(&mut conn).await?;

    let more = data.next_page_params().is_some();
    let versions = data.iter().map(|(v, ..)| v).collect::<Vec<_>>();
    let actions = VersionOwnerAction::for_versions(&conn, &versions).await?;
    let data = data
        .into_iter()
        .zip(actions)
        .map(|((v, cn, pb), voas)| (v, cn, pb, voas));

    let versions = data
        .into_iter()
        .map(|(version, crate_name, published_by, actions)| {
            EncodableVersion::from(version, &crate_name.name, published_by, actions)
        })
        .collect::<Vec<_>>();

    Ok((
        no_store(),
        Json(UpdatesResponse {
            versions,
            meta: UpdatesResponseMeta { more },
        }),
    ))
}
