use crate::model::data::user::{UserBaseSource, UserProfileExtSource, UserRelationExtSource};
use crate::model::processing::drive::DriveModel;
use crate::model::processing::emoji::EmojiModel;
use crate::model::processing::note::NoteModel;
use crate::model::processing::{get_mm_token_emoji, PackError, PackResult};
use crate::model::{PackType, PackingContext};
use crate::web::pagination::Pagination;
use crate::web::{AccessForbidden, ApiError};
use either::Either;
use futures_util::future::OptionFuture;
use futures_util::{StreamExt, TryStreamExt};
use magnetar_model::ck;
use magnetar_model::ck::sea_orm_active_enums::UserProfileFfvisibilityEnum;
use magnetar_model::user_model::{UserData, UserResolveOptions};
use magnetar_sdk::endpoints::user::{UserByIdReq, UserSelfReq};
use magnetar_sdk::mmm::Token;
use magnetar_sdk::types::drive::PackDriveFileBase;
use magnetar_sdk::types::emoji::EmojiContext;
use magnetar_sdk::types::instance::InstanceTicker;
use magnetar_sdk::types::user::{
    MovedTo, PackSecurityKeyBase, PackUserBase, PackUserMaybeAll, PackUserSelfMaybeAll,
    ProfileField, SecurityKeyBase, UserAuthOverviewExt, UserBase, UserDetailExt, UserProfileExt,
    UserProfilePinsEx, UserRelationExt, UserRelationship, UserSecretsExt, UserSelfExt,
};
use magnetar_sdk::types::{Id, MmXml};
use magnetar_sdk::{mmm, Optional, Packed, Required};
use serde::{Deserialize, Serialize};
use tokio::{join, try_join};
use tracing::warn;
use url::Url;

pub trait UserShapedData<'a>: Send + Sync {
    fn user(&self) -> &'a ck::user::Model;
    fn profile(&self) -> Option<&'a ck::user_profile::Model>;
    fn avatar(&self) -> Option<&'a ck::drive_file::Model>;
    fn banner(&self) -> Option<&'a ck::drive_file::Model>;
}

pub struct UserBorrowedData<'a> {
    pub user: &'a ck::user::Model,
    pub profile: Option<&'a ck::user_profile::Model>,
    pub avatar: Option<&'a ck::drive_file::Model>,
    pub banner: Option<&'a ck::drive_file::Model>,
}

impl<'a> UserShapedData<'a> for &'a UserData {
    fn user(&self) -> &'a ck::user::Model {
        &self.user
    }

    fn profile(&self) -> Option<&'a ck::user_profile::Model> {
        self.profile.as_ref()
    }

    fn avatar(&self) -> Option<&'a ck::drive_file::Model> {
        self.avatar.as_ref()
    }

    fn banner(&self) -> Option<&'a ck::drive_file::Model> {
        self.banner.as_ref()
    }
}

impl<'a> UserShapedData<'a> for UserBorrowedData<'a> {
    fn user(&self) -> &'a ck::user::Model {
        self.user
    }

    fn profile(&self) -> Option<&'a ck::user_profile::Model> {
        self.profile
    }

    fn avatar(&self) -> Option<&'a ck::drive_file::Model> {
        self.avatar
    }

    fn banner(&self) -> Option<&'a ck::drive_file::Model> {
        self.banner
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProfileFieldRaw<'a> {
    name: &'a str,
    value: &'a str,
}

pub struct UserModel;

impl UserModel {
    pub fn tokenize_username(&self, user: &ck::user::Model) -> Token {
        mmm::Context::default().parse_ui(user.name.as_deref().unwrap_or(&user.username))
    }

    pub fn tokenize_description(&self, user: &ck::user_profile::Model) -> Option<Token> {
        user.description
            .as_deref()
            .map(|d| mmm::Context::default().parse_inline(d))
    }

    pub fn get_effective_avatar_url(
        &self,
        ctx: &PackingContext,
        user: &ck::user::Model,
        avatar: Option<&PackDriveFileBase>,
    ) -> PackResult<Url> {
        Ok(avatar
            .and_then(
                |PackDriveFileBase {
                     file: Required(base),
                     ..
                 }| base.thumbnail_url.as_deref(),
            )
            .map(Url::parse)
            .and_then(|r| {
                if let Err(e) = r {
                    warn!("Failed to parse avatar URL: {e}");
                }

                r.ok().map(Ok)
            })
            .unwrap_or_else(|| {
                Url::parse(&format!(
                    "{}://{}/identicon/{}",
                    ctx.service.config.networking.protocol,
                    ctx.service.config.networking.host,
                    user.id
                ))
            })?)
    }

    pub async fn base_from_existing<'a>(
        &self,
        ctx: &PackingContext,
        user_data: &dyn UserShapedData<'a>,
    ) -> PackResult<PackUserBase> {
        let user = user_data.user();

        let drive_file_pack = DriveModel;
        let avatar = match (user_data.avatar(), &user.avatar_id) {
            (Some(avatar_file), _) => Some(drive_file_pack.pack_existing(ctx, avatar_file)),
            (None, Some(av_id)) => drive_file_pack.get_cached_base(ctx, av_id).await?,
            _ => None,
        };
        let avatar_url = &self.get_effective_avatar_url(ctx, user, avatar.as_ref())?;

        let username_mm = self.tokenize_username(user);

        let emoji_model = EmojiModel;
        let shortcodes = emoji_model.deduplicate_emoji(ctx, get_mm_token_emoji(&username_mm));
        let emojis = emoji_model
            .fetch_many_emojis(ctx, &shortcodes, user.host.as_deref())
            .await?;
        let instance = ctx
            .service
            .remote_instance_cache
            .get(
                user.host
                    .as_deref()
                    .unwrap_or(&ctx.service.config.networking.host),
            )
            .await?
            .map(|i| InstanceTicker::extract(ctx, i.as_ref()));
        let emoji_context = EmojiContext(emojis);

        let base = UserBase::extract(
            ctx,
            UserBaseSource {
                user,
                username_mm: mmm::to_xml_string(&username_mm).map(MmXml).as_ref().ok(),
                avatar_url,
                avatar: avatar.as_ref(),
                emoji_context: &emoji_context,
                instance: instance.as_ref(),
            },
        );

        Ok(PackUserBase::pack_from((
            Required(Id::from(&user.id)),
            Required(base),
        )))
    }

    async fn get_profile(
        &self,
        ctx: &PackingContext,
        user: &ck::user::Model,
    ) -> PackResult<ck::user_profile::Model> {
        ctx.service
            .db
            .get_user_profile_by_id(&user.id)
            .await?
            .ok_or_else(|| PackError::DataError("Missing user profile".to_string()))
    }

    pub async fn get_profile_by_id(
        &self,
        ctx: &PackingContext,
        id: &str,
    ) -> PackResult<ck::user_profile::Model> {
        ctx.service
            .db
            .get_user_profile_by_id(id)
            .await?
            .ok_or_else(|| PackError::DataError("Missing user profile".to_string()))
    }

    pub async fn profile_from_base<'a>(
        &self,
        ctx: &PackingContext,
        user_data: &dyn UserShapedData<'a>,
        profile: &ck::user_profile::Model,
        relation: Option<&UserRelationExt>,
        emoji_out: &mut EmojiContext,
    ) -> PackResult<UserProfileExt> {
        let user = user_data.user();

        let drive_file_pack = DriveModel;
        let banner = match (user_data.banner(), &user.banner_id) {
            (Some(banner_file), _) => Some(drive_file_pack.pack_existing(ctx, banner_file)),
            (None, Some(av_id)) => drive_file_pack.get_cached_base(ctx, av_id).await?,
            _ => None,
        };

        let moved = match &user.moved_to_uri {
            Some(uri) => {
                let moved = ctx.service.db.get_user_by_uri(uri).await?;

                moved.and_then(|m| {
                    Some(MovedTo {
                        moved_to_uri: m.uri?,
                        username: m.username,
                        host: m
                            .host
                            .unwrap_or_else(|| ctx.service.config.networking.host.to_string()),
                    })
                })
            }
            None => None,
        };

        let description_mm = self.tokenize_description(profile);

        let fields = Vec::<ProfileFieldRaw>::deserialize(&profile.fields)?;
        let parser = mmm::Context::new(2);
        let profile_fields = fields
            .into_iter()
            .map(|f| {
                let tok = parser.parse_profile_fields(f.value);

                ProfileField {
                    name: f.name.to_string(),
                    value: f.value.to_string(),
                    value_mm: mmm::to_xml_string(&tok).map(MmXml).ok(),
                    verified_at: None,
                }
            })
            .collect::<Vec<_>>();

        if let Some(desc_mm) = &description_mm {
            let emoji_model = EmojiModel;
            let shortcodes = emoji_model.deduplicate_emoji(ctx, get_mm_token_emoji(desc_mm));
            let emojis = emoji_model
                .fetch_many_emojis(ctx, &shortcodes, user.host.as_deref())
                .await?;

            emoji_out.extend_from(&emojis);
        }

        Ok(UserProfileExt::extract(
            ctx,
            UserProfileExtSource {
                user,
                profile,
                profile_fields: &profile_fields,
                banner: banner.as_ref(),
                description_mm: description_mm
                    .and_then(|desc_mm| mmm::to_xml_string(&desc_mm).map(MmXml).ok())
                    .as_ref(),
                relation,
                moved_to: moved.as_ref(),
            },
        ))
    }

    pub async fn secrets_from_base(
        &self,
        ctx: &PackingContext,
        user: &ck::user::Model,
        profile: &ck::user_profile::Model,
    ) -> PackResult<UserSecretsExt> {
        let secrets = ctx
            .service
            .db
            .get_user_security_keys_by_id(&user.id)
            .await?
            .into_iter()
            .map(|k| {
                PackSecurityKeyBase::pack_from((
                    Required(Id::from(&k.id)),
                    Required(SecurityKeyBase {
                        name: k.name,
                        last_used_at: Some(k.last_used.into()),
                    }),
                ))
            })
            .collect::<Vec<_>>();

        Ok(UserSecretsExt::extract(ctx, (profile, &secrets)))
    }

    pub async fn self_full_from_base<'a>(
        &self,
        ctx: &PackingContext,
        user_data: &impl UserShapedData<'a>,
        req: &UserSelfReq,
    ) -> PackResult<PackUserSelfMaybeAll> {
        let user = user_data.user();

        let should_fetch_profile = user_data.profile().is_none()
            && (req.profile.unwrap_or_default()
                || req.secrets.unwrap_or_default()
                || req.self_detail.unwrap_or_default());
        let profile_raw_promise =
            OptionFuture::from(should_fetch_profile.then(|| self.get_profile(ctx, user)));
        let (base_res, profile_res) =
            join!(self.base_from_existing(ctx, user_data), profile_raw_promise);
        let mut base = base_res?;
        let profile_raw = profile_res.transpose()?;
        let profile_ref = user_data.profile().or(profile_raw.as_ref());

        let detail = req
            .detail
            .unwrap_or_default()
            .then(|| UserDetailExt::extract(ctx, user));

        let profile = OptionFuture::from(req.profile.unwrap_or_default().then(|| {
            self.profile_from_base(
                ctx,
                user_data,
                profile_ref.unwrap(),
                None,
                &mut base.user.0.emojis,
            )
        }));

        let note_model = NoteModel {
            with_context: true,
            attachments: true,
        };
        let pins = OptionFuture::from(
            req.pins
                .unwrap_or_default()
                .then(|| note_model.fetch_pins(ctx, user)),
        );

        let secrets = OptionFuture::from(
            req.secrets
                .unwrap_or_default()
                .then(|| self.secrets_from_base(ctx, user, profile_ref.unwrap())),
        );

        let (profile_res, pins_res, secrets_res) = join!(profile, pins, secrets);

        let profile_resolved = profile_res.transpose()?;
        let pins_resolved = pins_res
            .transpose()?
            .map(|notes| UserProfilePinsEx::extract(ctx, &notes));
        let secrets_resolved = secrets_res.transpose()?;

        let self_info = req
            .self_detail
            .unwrap_or_default()
            .then(|| UserSelfExt::extract(ctx, (&user, profile_ref.unwrap())));

        Ok(PackUserSelfMaybeAll {
            id: base.id,
            user: base.user,
            profile: Optional(profile_resolved),
            pins: Optional(pins_resolved),
            detail: Optional(detail),
            secrets: Optional(secrets_resolved),
            self_detail: Optional(self_info),
        })
    }

    pub async fn relations_from_base(
        &self,
        ctx: &PackingContext,
        user: &ck::user::Model,
    ) -> PackResult<UserRelationExt> {
        let Some(me_user) = ctx.self_user.as_deref() else {
            return Ok(UserRelationExt {
                follows_you: false,
                you_follow: false,
                they_request_follow: false,
                you_request_follow: false,
                blocks_you: false,
                you_block: false,
                mute: false,
                mute_renotes: false,
            });
        };

        let me = Either::Right(me_user);
        let them = Either::Right(user);

        let (
            follow_in,
            follow_out,
            follow_request_in,
            follow_request_out,
            block_in,
            block_out,
            mute,
            renote_mute,
        ) = try_join!(
            ctx.is_relationship_between(them, me, UserRelationship::Follow),
            ctx.is_relationship_between(me, them, UserRelationship::Follow),
            ctx.is_relationship_between(them, me, UserRelationship::FollowRequest),
            ctx.is_relationship_between(me, them, UserRelationship::FollowRequest),
            ctx.is_relationship_between(them, me, UserRelationship::Block),
            ctx.is_relationship_between(me, them, UserRelationship::Block),
            ctx.is_relationship_between(me, them, UserRelationship::Mute),
            ctx.is_relationship_between(me, them, UserRelationship::RenoteMute)
        )?;

        Ok(UserRelationExt::extract(
            ctx,
            UserRelationExtSource {
                follow_in,
                follow_out,
                follow_request_in,
                follow_request_out,
                block_in,
                block_out,
                mute,
                renote_mute,
            },
        ))
    }

    pub fn auth_overview_from_profile(
        &self,
        ctx: &PackingContext,
        profile: &ck::user_profile::Model,
    ) -> PackResult<UserAuthOverviewExt> {
        Ok(UserAuthOverviewExt::extract(ctx, profile))
    }

    pub async fn foreign_full_from_base(
        &self,
        ctx: &PackingContext,
        user_data: &dyn UserShapedData<'_>,
        req: &UserByIdReq,
    ) -> PackResult<PackUserMaybeAll> {
        let user = user_data.user();

        let should_fetch_profile = req.profile.unwrap_or_default() || req.auth.unwrap_or_default();
        let profile_raw_promise =
            OptionFuture::from(should_fetch_profile.then(|| self.get_profile(ctx, user)));
        let (base, profile) = join!(self.base_from_existing(ctx, user_data), profile_raw_promise);
        let mut base = base?;
        let profile_raw = profile.transpose()?;

        let detail = req
            .detail
            .unwrap_or_default()
            .then(|| UserDetailExt::extract(ctx, user));

        let profile = OptionFuture::from(req.profile.unwrap_or_default().then(|| {
            self.profile_from_base(
                ctx,
                user_data,
                profile_raw.as_ref().unwrap(),
                None,
                &mut base.user.0.emojis,
            )
        }));

        let note_model = NoteModel {
            with_context: true,
            attachments: true,
        };
        let pins = OptionFuture::from(
            req.pins
                .unwrap_or_default()
                .then(|| note_model.fetch_pins(ctx, user)),
        );
        let relations = OptionFuture::from(
            req.relation
                .unwrap_or_default()
                .then(|| self.relations_from_base(ctx, user)),
        );

        let auth = req
            .auth
            .unwrap_or_default()
            .then(|| self.auth_overview_from_profile(ctx, profile_raw.as_ref().unwrap()))
            .transpose()?;

        let (profile_res, pins_res, relations_res) = join!(profile, pins, relations);

        let profile_resolved = profile_res.transpose()?;
        let pins_resolved = pins_res
            .transpose()?
            .map(|notes| UserProfilePinsEx::extract(ctx, &notes));
        let relations_resolved = relations_res.transpose()?;

        Ok(PackUserMaybeAll {
            id: base.id,
            user: base.user,
            profile: Optional(profile_resolved),
            pins: Optional(pins_resolved),
            detail: Optional(detail),
            relation: Optional(relations_resolved),
            auth: Optional(auth),
        })
    }

    pub async fn pack_many_base(
        &self,
        ctx: &PackingContext,
        users: &[&dyn UserShapedData<'_>],
    ) -> PackResult<Vec<PackUserBase>> {
        let futures = users
            .iter()
            .map(|&user| self.base_from_existing(ctx, user))
            .collect::<Vec<_>>();

        let users_proc = futures::stream::iter(futures)
            .buffered(20)
            .err_into::<PackError>()
            .try_collect::<Vec<_>>()
            .await?;

        Ok(users_proc)
    }

    pub async fn pack_many_maybe_full(
        &self,
        ctx: &PackingContext,
        users: &[&dyn UserShapedData<'_>],
        req: &UserByIdReq,
    ) -> PackResult<Vec<PackUserMaybeAll>> {
        let futures = users
            .iter()
            .map(|&user| self.foreign_full_from_base(ctx, user, req))
            .collect::<Vec<_>>();

        let users_proc = futures::stream::iter(futures)
            .buffered(20)
            .err_into::<PackError>()
            .try_collect::<Vec<_>>()
            .await?;

        Ok(users_proc)
    }

    pub async fn follower_visibility_check(
        &self,
        ctx: &PackingContext,
        id: &str,
    ) -> Result<(), ApiError> {
        if ctx.is_id_self(id) {
            return Ok(());
        }

        let profile = self.get_profile_by_id(ctx, id).await?;

        match (&ctx.self_user, &profile.ff_visibility) {
            (_, UserProfileFfvisibilityEnum::Public) => {}
            (Some(self_user), UserProfileFfvisibilityEnum::Followers)
                if ctx
                    .is_relationship_between(
                        Either::Right(self_user),
                        Either::Left(id),
                        UserRelationship::Follow,
                    )
                    .await? => {}
            _ => {
                Err(AccessForbidden(
                    "Follower information not visible".to_string(),
                ))?;
            }
        }

        Ok(())
    }

    pub async fn get_followers(
        &self,
        ctx: &PackingContext,
        id: &str,
        pagination: &mut Pagination,
    ) -> Result<Vec<PackUserMaybeAll>, ApiError> {
        self.follower_visibility_check(ctx, id).await?;

        let users = ctx
            .service
            .db
            .get_user_resolver()
            .get_followers(
                &UserResolveOptions {
                    with_avatar: true,
                    with_banner: true,
                    with_profile: true,
                },
                id,
                &pagination.current,
                &mut pagination.prev,
                &mut pagination.next,
                pagination.limit.into(),
            )
            .await?;

        let users_ref = users
            .iter()
            .map(|u| Box::new(u) as Box<dyn UserShapedData<'_>>)
            .collect::<Vec<_>>();

        Ok(self
            .pack_many_maybe_full(
                ctx,
                &users_ref.iter().map(Box::as_ref).collect::<Vec<_>>(),
                &UserByIdReq {
                    profile: Some(true),
                    auth: None,
                    detail: None,
                    pins: None,
                    relation: None,
                },
            )
            .await?)
    }

    pub async fn get_followees(
        &self,
        ctx: &PackingContext,
        id: &str,
        pagination: &mut Pagination,
    ) -> Result<Vec<PackUserMaybeAll>, ApiError> {
        self.follower_visibility_check(ctx, id).await?;

        let users = ctx
            .service
            .db
            .get_user_resolver()
            .get_followees(
                &UserResolveOptions {
                    with_avatar: true,
                    with_banner: true,
                    with_profile: true,
                },
                id,
                &pagination.current,
                &mut pagination.prev,
                &mut pagination.next,
                pagination.limit.into(),
            )
            .await?;

        let users_ref = users
            .iter()
            .map(|u| Box::new(u) as Box<dyn UserShapedData<'_>>)
            .collect::<Vec<_>>();

        Ok(self
            .pack_many_maybe_full(
                ctx,
                &users_ref.iter().map(Box::as_ref).collect::<Vec<_>>(),
                &UserByIdReq {
                    profile: Some(true),
                    auth: None,
                    detail: None,
                    pins: None,
                    relation: None,
                },
            )
            .await?)
    }

    pub async fn get_follow_requests(
        &self,
        ctx: &PackingContext,
        id: &str,
        pagination: &mut Pagination,
    ) -> Result<Vec<PackUserMaybeAll>, ApiError> {
        let users = ctx
            .service
            .db
            .get_user_resolver()
            .get_follow_requests(
                &UserResolveOptions {
                    with_avatar: true,
                    with_banner: true,
                    with_profile: true,
                },
                id,
                &pagination.current,
                &mut pagination.prev,
                &mut pagination.next,
                pagination.limit.into(),
            )
            .await?;

        let users_ref = users
            .iter()
            .map(|u| Box::new(u) as Box<dyn UserShapedData<'_>>)
            .collect::<Vec<_>>();

        Ok(self
            .pack_many_maybe_full(
                ctx,
                &users_ref.iter().map(Box::as_ref).collect::<Vec<_>>(),
                &UserByIdReq {
                    profile: Some(true),
                    auth: None,
                    detail: None,
                    pins: None,
                    relation: None,
                },
            )
            .await?)
    }
}
