use crate::model_ext::{
    AliasColumnExt, AliasSourceExt, AliasSuffixExt, CursorPaginationExt, EntityPrefixExt, MagIden,
    ModelPagination, SelectColumnsExt,
};
use crate::note_model::data::NoteData;
use crate::note_model::{NoteResolveOptions, NoteResolver};
use crate::user_model::{UserData, UserResolveOptions, UserResolver};
use crate::{CalckeyDbError, CalckeyModel};
use chrono::{DateTime, Utc};
use ck::sea_orm_active_enums::NotificationTypeEnum;
use ck::{access_token, notification, user};
use ext_model_migration::{JoinType, SelectStatement};
use magnetar_sdk::types::SpanFilter;
use sea_orm::prelude::Expr;
use sea_orm::sea_query::{IntoCondition, Query};
use sea_orm::{ActiveEnum, Iden, QueryTrait};
use sea_orm::{DbErr, EntityTrait, FromQueryResult, QueryFilter, QueryResult, QuerySelect};
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NotificationData {
    pub notification: notification::Model,
    pub access_token: Option<access_token::Model>,
    pub notifier: Option<UserData>,
    pub notification_note: Option<NoteData>,
}

impl FromQueryResult for NotificationData {
    fn from_query_result(res: &QueryResult, prefix: &str) -> Result<Self, DbErr> {
        let prefix = if prefix.is_empty() {
            notification::Entity.base_prefix()
        } else {
            MagIden::alias(prefix)
        };

        Ok(NotificationData {
            notification: notification::Model::from_query_result(res, &prefix.to_string())?,
            access_token: access_token::Model::from_query_result_optional(
                res,
                &prefix.join_str_as_str(ACCESS_TOKEN),
            )?,
            notification_note: NoteData::from_query_result_optional(
                res,
                &prefix.join_str_as_str(NOTIFICATION_NOTE),
            )?,
            notifier: UserData::from_query_result_optional(res, &prefix.join_str_as_str(NOTIFIER))?,
        })
    }
}

impl ModelPagination for NotificationData {
    fn id(&self) -> &str {
        &self.notification.id
    }

    fn time(&self) -> DateTime<Utc> {
        self.notification.created_at.into()
    }
}

const NOTIFIER: &str = "notifier.";
const NOTIFICATION_NOTE: &str = "note.";
const ACCESS_TOKEN: &str = "access_token.";

pub struct NotificationResolveOptions {
    pub note_options: NoteResolveOptions,
    pub user_options: UserResolveOptions,
}

#[derive(Clone)]
pub struct NotificationResolver {
    db: CalckeyModel,
    note_resolver: NoteResolver,
    user_resolver: UserResolver,
}

impl NotificationResolver {
    pub(crate) fn new(
        db: CalckeyModel,
        user_resolver: UserResolver,
        note_resolver: NoteResolver,
    ) -> Self {
        Self {
            db,
            note_resolver,
            user_resolver,
        }
    }

    pub fn resolve(
        &self,
        q: &mut SelectStatement,
        notification_tbl: &MagIden,
        resolve_options: &NotificationResolveOptions,
        note_resolver: &NoteResolver,
        user_resolver: &UserResolver,
    ) {
        q.add_aliased_columns::<notification::Entity>(notification_tbl);

        let notifier_tbl = notification_tbl.join_str(NOTIFIER);
        q.add_aliased_columns::<user::Entity>(&notifier_tbl);
        q.join_columns(
            JoinType::LeftJoin,
            notification::Relation::User2.with_from_alias(notification_tbl),
            &notifier_tbl,
        );
        user_resolver.resolve(q, &notifier_tbl, &resolve_options.user_options);

        let token_tbl = notification_tbl.join_str(ACCESS_TOKEN);
        q.add_aliased_columns::<access_token::Entity>(&token_tbl);
        q.join_columns(
            JoinType::LeftJoin,
            notification::Relation::AccessToken.with_from_alias(notification_tbl),
            &token_tbl,
        );

        let note_tbl = notification_tbl.join_str(NOTIFICATION_NOTE);
        q.join_columns(
            JoinType::LeftJoin,
            notification::Relation::Note.with_from_alias(notification_tbl),
            &note_tbl,
        );
        note_resolver.attach_note(
            q,
            &note_tbl,
            1,
            1,
            &resolve_options.note_options,
            &self.user_resolver,
        );
    }

    pub async fn get_single(
        &self,
        resolve_options: &NotificationResolveOptions,
        notification_id: &str,
    ) -> Result<Option<NotificationData>, CalckeyDbError> {
        let notification_tbl = notification::Entity.base_prefix();

        let mut query = Query::select();
        query.from_as(notification::Entity, notification_tbl.clone());

        self.resolve(
            &mut query,
            &notification_tbl,
            resolve_options,
            &self.note_resolver,
            &self.user_resolver,
        );

        let mut select = notification::Entity::find();
        *QuerySelect::query(&mut select) = query;

        let notifications = select
            .filter(
                notification_tbl
                    .col(notification::Column::Id)
                    .eq(notification_id),
            )
            .into_model::<NotificationData>()
            .one(self.db.inner())
            .await?;

        Ok(notifications)
    }

    pub async fn get(
        &self,
        resolve_options: &NotificationResolveOptions,
        user_id: &str,
        notification_types: &[NotificationTypeEnum],
        unread_only: bool,
        pagination: &SpanFilter,
        prev: &mut Option<SpanFilter>,
        next: &mut Option<SpanFilter>,
        limit: u64,
    ) -> Result<Vec<NotificationData>, CalckeyDbError> {
        let notification_tbl = notification::Entity.base_prefix();

        let mut query = Query::select();
        query.from_as(notification::Entity, notification_tbl.clone());

        self.resolve(
            &mut query,
            &notification_tbl,
            resolve_options,
            &self.note_resolver,
            &self.user_resolver,
        );

        let mut select = notification::Entity::find();
        *QuerySelect::query(&mut select) = query;

        let notifications = select
            .filter(
                notification_tbl
                    .col(notification::Column::NotifieeId)
                    .eq(user_id)
                    .and(
                        notification_tbl.col(notification::Column::Type).is_in(
                            notification_types
                                .iter()
                                .copied()
                                .map(Expr::val)
                                .map(|e| e.cast_as(NotificationTypeEnum::name())),
                        ),
                    ),
            )
            .apply_if(unread_only.then_some(()), |s, _| {
                s.filter(
                    notification_tbl
                        .col(notification::Column::IsRead)
                        .not()
                        .into_condition(),
                )
            })
            .get_paginated_model::<NotificationData, _, _>(
                &self.db.0,
                Some(notification_tbl),
                (notification::Column::CreatedAt, notification::Column::Id),
                pagination,
                prev,
                next,
                limit,
            )
            .await?;

        Ok(notifications)
    }
}
