use crate::model::processing::notification::NotificationModel;
use crate::model::processing::user::{UserBorrowedData, UserModel, UserShapedData};
use crate::model::PackingContext;
use crate::service::MagnetarService;
use crate::web::auth::{AuthenticatedUser, MaybeUser};
use crate::web::pagination::Pagination;
use crate::web::{ApiError, ArgumentOutOfRange, ObjectNotFound};
use axum::extract::{Path, Query, State};
use axum::Json;
use itertools::Itertools;
use magnetar_common::util::lenient_parse_tag_decode;
use magnetar_sdk::endpoints::user::{
    GetFollowRequestsSelf, GetFollowersById, GetFollowersSelf, GetFollowingById, GetFollowingSelf,
    GetManyUsersById, GetNotifications, GetUserByAcct, GetUserById, GetUserSelf, ManyUsersByIdReq,
    NotificationsReq,
};
use magnetar_sdk::endpoints::{Req, Res};
use magnetar_sdk::types::notification::NotificationType;
use std::collections::HashMap;
use std::sync::Arc;
use strum::IntoEnumIterator;

#[tracing::instrument(err, skip_all)]
pub async fn handle_user_info_self(
    Query(req): Query<Req<GetUserSelf>>,
    State(service): State<Arc<MagnetarService>>,
    AuthenticatedUser(user): AuthenticatedUser,
) -> Result<Json<Res<GetUserSelf>>, ApiError> {
    let ctx = PackingContext::new(service, Some(user.clone())).await?;
    let user = UserModel
        .self_full_from_base(
            &ctx,
            &UserBorrowedData {
                user: user.as_ref(),
                profile: None,
                avatar: None,
                banner: None,
            },
            &req,
        )
        .await?;
    Ok(Json(user))
}

#[tracing::instrument(err, skip_all)]
pub async fn handle_user_info(
    Path(id): Path<String>,
    Query(req): Query<Req<GetUserById>>,
    State(service): State<Arc<MagnetarService>>,
    MaybeUser(self_user): MaybeUser,
) -> Result<Json<Res<GetUserById>>, ApiError> {
    let ctx = PackingContext::new(service.clone(), self_user).await?;
    let user_model = service
        .db
        .get_user_by_id(&id)
        .await?
        .ok_or(ObjectNotFound(id))?;

    let user = UserModel
        .foreign_full_from_base(
            &ctx,
            &UserBorrowedData {
                user: &user_model,
                profile: None,
                avatar: None,
                banner: None,
            },
            &req,
        )
        .await?;
    Ok(Json(user))
}

#[tracing::instrument(err, skip_all)]
pub async fn handle_user_info_by_acct(
    Path(tag_str): Path<String>,
    Query(req): Query<Req<GetUserByAcct>>,
    State(service): State<Arc<MagnetarService>>,
    MaybeUser(self_user): MaybeUser,
) -> Result<Json<Res<GetUserByAcct>>, ApiError> {
    let mut tag = lenient_parse_tag_decode(&tag_str)?;
    if matches!(&tag.host, Some(host) if host.to_string() == service.config.networking.host) {
        tag.host = None;
    }

    let ctx = PackingContext::new(service.clone(), self_user).await?;
    let user_model = service
        .db
        .get_user_by_tag(tag.name.as_ref(), tag.host.as_ref())
        .await?
        .ok_or(ObjectNotFound(tag_str))?;

    let user = UserModel
        .foreign_full_from_base(
            &ctx,
            &UserBorrowedData {
                user: &user_model,
                profile: None,
                avatar: None,
                banner: None,
            },
            &req,
        )
        .await?;
    Ok(Json(user))
}

#[tracing::instrument(err, skip_all)]
pub async fn handle_user_by_id_many(
    Query(ManyUsersByIdReq { id }): Query<Req<GetManyUsersById>>,
    State(service): State<Arc<MagnetarService>>,
    MaybeUser(user): MaybeUser,
) -> Result<Json<Res<GetManyUsersById>>, ApiError> {
    if id.len() >= 100 {
        return Err(ArgumentOutOfRange(stringify!(id).to_string()).into());
    }

    let users = service
        .db
        .get_many_users_by_id(&id.iter().cloned().sorted().dedup().collect::<Vec<_>>())
        .await?;

    let ctx = PackingContext::new(service, user.clone()).await?;
    let user_model = UserModel;

    let user_data = users
        .iter()
        .map(|user| UserBorrowedData {
            user,
            profile: None,
            avatar: None,
            banner: None,
        })
        .map(|u| Box::new(u) as Box<dyn UserShapedData>)
        .collect::<Vec<_>>();

    let users_proc = user_model
        .pack_many_base(&ctx, &user_data.iter().map(Box::as_ref).collect::<Vec<_>>())
        .await?
        .into_iter()
        .map(|u| (u.id.0.id.clone(), u))
        .collect::<HashMap<_, _>>();

    let users_ordered = id
        .iter()
        .map(|ident| users_proc.get(ident).cloned())
        .collect::<Vec<_>>();

    Ok(Json(users_ordered))
}

#[tracing::instrument(err, skip_all)]
pub async fn handle_notifications(
    Query(NotificationsReq {
        ref exclude_types,
        include_types,
        unread_only,
    }): Query<Req<GetNotifications>>,
    State(service): State<Arc<MagnetarService>>,
    AuthenticatedUser(user): AuthenticatedUser,
    mut pagination: Pagination,
) -> Result<(Pagination, Json<Res<GetNotifications>>), ApiError> {
    let notification_types = include_types
        .unwrap_or_else(|| NotificationType::iter().collect::<Vec<_>>())
        .iter()
        .filter(|t| {
            exclude_types.is_none() || !exclude_types.as_ref().is_some_and(|tt| tt.contains(t))
        })
        .copied()
        .collect::<Vec<_>>();

    let ctx = PackingContext::new(service, Some(user.clone())).await?;
    let notification_model = NotificationModel;
    let notifications = notification_model
        .get_notifications(
            &ctx,
            &user.id,
            &notification_types,
            unread_only.unwrap_or_default(),
            &mut pagination,
        )
        .await?;

    Ok((pagination, Json(notifications)))
}

#[tracing::instrument(err, skip_all)]
pub async fn handle_following_self(
    Query(_): Query<Req<GetFollowingSelf>>,
    State(service): State<Arc<MagnetarService>>,
    AuthenticatedUser(user): AuthenticatedUser,
    mut pagination: Pagination,
) -> Result<(Pagination, Json<Res<GetFollowingSelf>>), ApiError> {
    let ctx = PackingContext::new(service, Some(user.clone())).await?;
    let user_model = UserModel;
    let users = user_model
        .get_followees(&ctx, &user.id, &mut pagination)
        .await?;

    Ok((pagination, Json(users)))
}

#[tracing::instrument(err, skip_all)]
pub async fn handle_followers_self(
    Query(_): Query<Req<GetFollowersSelf>>,
    State(service): State<Arc<MagnetarService>>,
    AuthenticatedUser(user): AuthenticatedUser,
    mut pagination: Pagination,
) -> Result<(Pagination, Json<Res<GetFollowersSelf>>), ApiError> {
    let ctx = PackingContext::new(service, Some(user.clone())).await?;
    let user_model = UserModel;
    let users = user_model
        .get_followers(&ctx, &user.id, &mut pagination)
        .await?;

    Ok((pagination, Json(users)))
}

#[tracing::instrument(err, skip_all)]
pub async fn handle_following(
    Query(_): Query<Req<GetFollowingById>>,
    Path(id): Path<String>,
    State(service): State<Arc<MagnetarService>>,
    MaybeUser(user): MaybeUser,
    mut pagination: Pagination,
) -> Result<(Pagination, Json<Res<GetFollowingById>>), ApiError> {
    let ctx = PackingContext::new(service, user.clone()).await?;
    let user_model = UserModel;
    let users = user_model.get_followees(&ctx, &id, &mut pagination).await?;

    Ok((pagination, Json(users)))
}

#[tracing::instrument(err, skip_all)]
pub async fn handle_followers(
    Query(_): Query<Req<GetFollowersById>>,
    Path(id): Path<String>,
    State(service): State<Arc<MagnetarService>>,
    MaybeUser(user): MaybeUser,
    mut pagination: Pagination,
) -> Result<(Pagination, Json<Res<GetFollowingById>>), ApiError> {
    let ctx = PackingContext::new(service, user.clone()).await?;
    let user_model = UserModel;
    let users = user_model.get_followers(&ctx, &id, &mut pagination).await?;

    Ok((pagination, Json(users)))
}

#[tracing::instrument(err, skip_all)]
pub async fn handle_follow_requests_self(
    Query(_): Query<Req<GetFollowRequestsSelf>>,
    State(service): State<Arc<MagnetarService>>,
    AuthenticatedUser(user): AuthenticatedUser,
    mut pagination: Pagination,
) -> Result<(Pagination, Json<Res<GetFollowingById>>), ApiError> {
    let ctx = PackingContext::new(service, Some(user.clone())).await?;
    let user_model = UserModel;
    let users = user_model
        .get_follow_requests(&ctx, &user.id, &mut pagination)
        .await?;

    Ok((pagination, Json(users)))
}
