use std::collections::HashMap;

use db::sqlx;
use model::users::UserModel;
use serde::{Deserialize, Serialize};
use session::Session;

use crate::{AppService, Pagination, error::AppError, non_empty, session_user};

#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct UserRelationStatus {
    pub username: String,
    pub avatar_url: Option<String>,
    pub is_following: bool,
    pub is_followed_by: bool,
    pub is_blocked: bool,
    pub has_blocked_me: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct UserRelationCard {
    pub username: String,
    pub display_name: Option<String>,
    pub avatar_url: Option<String>,
    pub is_following: bool,
    pub is_blocked: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct UserRelationCounts {
    pub followers: i64,
    pub following: i64,
    pub blocked: i64,
}

impl AppService {
    pub async fn users_follow_by_username(
        &self,
        ctx: &Session,
        username: &str,
    ) -> Result<UserRelationStatus, AppError> {
        let current_uid = session_user(ctx)?;
        let target = self.users_relation_target(current_uid, username).await?;

        if self.users_is_blocked(current_uid, target.id).await?
            || self.users_is_blocked(target.id, current_uid).await?
        {
            return Err(AppError::Forbidden(
                "cannot follow a blocked user".to_string(),
            ));
        }

        if !self.users_is_following(current_uid, target.id).await? {
            sqlx::query(
                "INSERT INTO user_favorite (\"user\", target, created_at) VALUES ($1, $2, $3)",
            )
            .bind(current_uid)
            .bind(target.id)
            .bind(chrono::Utc::now())
            .execute(self.db.writer())
            .await
            .map_err(|e| AppError::DatabaseError(e.to_string()))?;
        }

        self.users_relation_status_for(current_uid, target).await
    }

    pub async fn users_unfollow_by_username(
        &self,
        ctx: &Session,
        username: &str,
    ) -> Result<UserRelationStatus, AppError> {
        let current_uid = session_user(ctx)?;
        let target = self.users_relation_target(current_uid, username).await?;

        sqlx::query(
            "DELETE FROM user_favorite WHERE \"user\" = $1 AND target = $2",
        )
        .bind(current_uid)
        .bind(target.id)
        .execute(self.db.writer())
        .await
        .map_err(|e| AppError::DatabaseError(e.to_string()))?;

        self.users_relation_status_for(current_uid, target).await
    }

    pub async fn users_block_by_username(
        &self,
        ctx: &Session,
        username: &str,
    ) -> Result<UserRelationStatus, AppError> {
        let current_uid = session_user(ctx)?;
        let target = self.users_relation_target(current_uid, username).await?;

        if !self.users_is_blocked(current_uid, target.id).await? {
            sqlx::query(
                "INSERT INTO user_blacklist (\"user\", black, created_at) VALUES ($1, $2, $3)",
            )
            .bind(current_uid)
            .bind(target.id)
            .bind(chrono::Utc::now())
            .execute(self.db.writer())
            .await
            .map_err(|e| AppError::DatabaseError(e.to_string()))?;
        }

        sqlx::query(
            "DELETE FROM user_favorite \
             WHERE (\"user\" = $1 AND target = $2) OR (\"user\" = $2 AND target = $1)",
        )
        .bind(current_uid)
        .bind(target.id)
        .execute(self.db.writer())
        .await
        .map_err(|e| AppError::DatabaseError(e.to_string()))?;

        self.users_relation_status_for(current_uid, target).await
    }

    pub async fn users_unblock_by_username(
        &self,
        ctx: &Session,
        username: &str,
    ) -> Result<UserRelationStatus, AppError> {
        let current_uid = session_user(ctx)?;
        let target = self.users_relation_target(current_uid, username).await?;

        sqlx::query(
            "DELETE FROM user_blacklist WHERE \"user\" = $1 AND black = $2",
        )
        .bind(current_uid)
        .bind(target.id)
        .execute(self.db.writer())
        .await
        .map_err(|e| AppError::DatabaseError(e.to_string()))?;

        self.users_relation_status_for(current_uid, target).await
    }

    pub async fn users_relation_status_by_username(
        &self,
        ctx: &Session,
        username: &str,
    ) -> Result<UserRelationStatus, AppError> {
        let current_uid = session_user(ctx)?;
        let target = self.users_relation_target(current_uid, username).await?;
        self.users_relation_status_for(current_uid, target).await
    }

    pub async fn users_followers_by_username(
        &self,
        ctx: Option<&Session>,
        username: &str,
        pagination: Pagination,
    ) -> Result<Vec<UserRelationCard>, AppError> {
        let target = self.users_find_active_user_by_username(username).await?;
        let current_uid = ctx.and_then(Session::user);
        let users = sqlx::query_as::<_, UserModel>(
            "SELECT u.id, u.username, u.display_name, u.avatar_url, u.website_url, u.allow_use, u.can_search, \
                    u.last_sign_in_at, u.created_at, u.updated_at \
             FROM user_favorite f \
             INNER JOIN \"user\" u ON u.id = f.\"user\" \
             WHERE f.target = $1 AND u.allow_use = true \
             ORDER BY f.created_at DESC \
             LIMIT $2 OFFSET $3",
        )
        .bind(target.id)
        .bind(pagination.limit() as i64)
        .bind(pagination.offset() as i64)
        .fetch_all(self.db.reader())
        .await
        .map_err(|e| AppError::DatabaseError(e.to_string()))?;

        self.users_relation_cards(current_uid, users).await
    }

    pub async fn users_following_by_username(
        &self,
        ctx: Option<&Session>,
        username: &str,
        pagination: Pagination,
    ) -> Result<Vec<UserRelationCard>, AppError> {
        let target = self.users_find_active_user_by_username(username).await?;
        let current_uid = ctx.and_then(Session::user);
        let users = sqlx::query_as::<_, UserModel>(
            "SELECT u.id, u.username, u.display_name, u.avatar_url, u.website_url, u.allow_use, u.can_search, \
                    u.last_sign_in_at, u.created_at, u.updated_at \
             FROM user_favorite f \
             INNER JOIN \"user\" u ON u.id = f.target \
             WHERE f.\"user\" = $1 AND u.allow_use = true \
             ORDER BY f.created_at DESC \
             LIMIT $2 OFFSET $3",
        )
        .bind(target.id)
        .bind(pagination.limit() as i64)
        .bind(pagination.offset() as i64)
        .fetch_all(self.db.reader())
        .await
        .map_err(|e| AppError::DatabaseError(e.to_string()))?;

        self.users_relation_cards(current_uid, users).await
    }

    pub async fn users_blocked(
        &self,
        ctx: &Session,
    ) -> Result<Vec<UserRelationCard>, AppError> {
        let current_uid = session_user(ctx)?;
        let users = sqlx::query_as::<_, UserModel>(
            "SELECT u.id, u.username, u.display_name, u.avatar_url, u.website_url, u.allow_use, u.can_search, \
                    u.last_sign_in_at, u.created_at, u.updated_at \
             FROM user_blacklist b \
             INNER JOIN \"user\" u ON u.id = b.black \
             WHERE b.\"user\" = $1 AND u.allow_use = true \
             ORDER BY b.created_at DESC",
        )
        .bind(current_uid)
        .fetch_all(self.db.reader())
        .await
        .map_err(|e| AppError::DatabaseError(e.to_string()))?;

        self.users_relation_cards(Some(current_uid), users).await
    }

    pub async fn users_relation_counts_by_username(
        &self,
        username: &str,
    ) -> Result<UserRelationCounts, AppError> {
        let target = self.users_find_active_user_by_username(username).await?;
        let followers = sqlx::query_scalar::<_, i64>(
            "SELECT COUNT(*) FROM user_favorite f \
             INNER JOIN \"user\" u ON u.id = f.\"user\" \
             WHERE f.target = $1 AND u.allow_use = true",
        )
        .bind(target.id)
        .fetch_one(self.db.reader())
        .await
        .map_err(|e| AppError::DatabaseError(e.to_string()))?;
        let following = sqlx::query_scalar::<_, i64>(
            "SELECT COUNT(*) FROM user_favorite f \
             INNER JOIN \"user\" u ON u.id = f.target \
             WHERE f.\"user\" = $1 AND u.allow_use = true",
        )
        .bind(target.id)
        .fetch_one(self.db.reader())
        .await
        .map_err(|e| AppError::DatabaseError(e.to_string()))?;
        let blocked = sqlx::query_scalar::<_, i64>(
            "SELECT COUNT(*) FROM user_blacklist b \
             INNER JOIN \"user\" u ON u.id = b.black \
             WHERE b.\"user\" = $1 AND u.allow_use = true",
        )
        .bind(target.id)
        .fetch_one(self.db.reader())
        .await
        .map_err(|e| AppError::DatabaseError(e.to_string()))?;

        Ok(UserRelationCounts {
            followers,
            following,
            blocked,
        })
    }

    async fn users_relation_target(
        &self,
        current_uid: uuid::Uuid,
        username: &str,
    ) -> Result<UserModel, AppError> {
        let target = self.users_find_active_user_by_username(username).await?;
        if target.id == current_uid {
            return Err(AppError::BadRequest(
                "cannot operate on yourself".to_string(),
            ));
        }
        Ok(target)
    }

    async fn users_relation_status_for(
        &self,
        current_uid: uuid::Uuid,
        target: UserModel,
    ) -> Result<UserRelationStatus, AppError> {
        Ok(UserRelationStatus {
            username: target.username,
            avatar_url: non_empty(target.avatar_url),
            is_following: self
                .users_is_following(current_uid, target.id)
                .await?,
            is_followed_by: self
                .users_is_following(target.id, current_uid)
                .await?,
            is_blocked: self.users_is_blocked(current_uid, target.id).await?,
            has_blocked_me: self
                .users_is_blocked(target.id, current_uid)
                .await?,
        })
    }

    async fn users_relation_cards(
        &self,
        current_uid: Option<uuid::Uuid>,
        users: Vec<UserModel>,
    ) -> Result<Vec<UserRelationCard>, AppError> {
        let user_ids = users.iter().map(|user| user.id).collect::<Vec<_>>();
        let following = match current_uid {
            Some(current_uid) => {
                self.users_following_set(current_uid, &user_ids).await?
            }
            None => HashMap::new(),
        };
        let blocked = match current_uid {
            Some(current_uid) => {
                self.users_blocked_set(current_uid, &user_ids).await?
            }
            None => HashMap::new(),
        };

        Ok(users
            .into_iter()
            .map(|user| UserRelationCard {
                username: user.username,
                display_name: non_empty(user.display_name),
                avatar_url: non_empty(user.avatar_url),
                is_following: following.contains_key(&user.id),
                is_blocked: blocked.contains_key(&user.id),
            })
            .collect())
    }

    async fn users_is_following(
        &self,
        user_uid: uuid::Uuid,
        target_uid: uuid::Uuid,
    ) -> Result<bool, AppError> {
        let exists = sqlx::query_scalar::<_, bool>(
            "SELECT EXISTS(SELECT 1 FROM user_favorite WHERE \"user\" = $1 AND target = $2)",
        )
        .bind(user_uid)
        .bind(target_uid)
        .fetch_one(self.db.reader())
        .await
        .map_err(|e| AppError::DatabaseError(e.to_string()))?;
        Ok(exists)
    }

    async fn users_is_blocked(
        &self,
        user_uid: uuid::Uuid,
        target_uid: uuid::Uuid,
    ) -> Result<bool, AppError> {
        let exists = sqlx::query_scalar::<_, bool>(
            "SELECT EXISTS(SELECT 1 FROM user_blacklist WHERE \"user\" = $1 AND black = $2)",
        )
        .bind(user_uid)
        .bind(target_uid)
        .fetch_one(self.db.reader())
        .await
        .map_err(|e| AppError::DatabaseError(e.to_string()))?;
        Ok(exists)
    }

    async fn users_following_set(
        &self,
        user_uid: uuid::Uuid,
        target_uids: &[uuid::Uuid],
    ) -> Result<HashMap<uuid::Uuid, ()>, AppError> {
        if target_uids.is_empty() {
            return Ok(HashMap::new());
        }
        let rows = sqlx::query_as::<_, (uuid::Uuid,)>(
            "SELECT target FROM user_favorite WHERE \"user\" = $1 AND target = ANY($2)",
        )
        .bind(user_uid)
        .bind(target_uids)
        .fetch_all(self.db.reader())
        .await
        .map_err(|e| AppError::DatabaseError(e.to_string()))?;
        Ok(rows.into_iter().map(|(uid,)| (uid, ())).collect())
    }

    async fn users_blocked_set(
        &self,
        user_uid: uuid::Uuid,
        target_uids: &[uuid::Uuid],
    ) -> Result<HashMap<uuid::Uuid, ()>, AppError> {
        if target_uids.is_empty() {
            return Ok(HashMap::new());
        }
        let rows = sqlx::query_as::<_, (uuid::Uuid,)>(
            "SELECT black FROM user_blacklist WHERE \"user\" = $1 AND black = ANY($2)",
        )
        .bind(user_uid)
        .bind(target_uids)
        .fetch_all(self.db.reader())
        .await
        .map_err(|e| AppError::DatabaseError(e.to_string()))?;
        Ok(rows.into_iter().map(|(uid,)| (uid, ())).collect())
    }
}
