use chrono::Utc;
use uuid::Uuid;

use crate::event::{RoomInfo, UserInfo, star};
use crate::{ChannelBus, ChannelResult};

use super::WsHandler;
use super::WsOutEvent;

impl WsHandler {
    pub(super) async fn message_star(
        bus: &ChannelBus,
        user_id: Uuid,
        room: Uuid,
        message: Uuid,
        do_star: bool,
    ) -> ChannelResult<Option<WsOutEvent>> {
        Self::ensure_room_access(bus, user_id, room).await?;
        Self::ensure_message_in_room(bus, room, message).await?;

        let room_info = bus
            .lookup_room(room)
            .await
            .unwrap_or_else(|_| RoomInfo::unknown(room));
        let user_info = bus
            .lookup_user(user_id)
            .await
            .unwrap_or_else(|_| UserInfo::unknown(user_id));

        if do_star {
            let result = db::sqlx::query(
                "INSERT INTO message_star (message, room, \"user\", created_at) \
                 VALUES ($1, $2, $3, now()) ON CONFLICT (message, \"user\") DO NOTHING",
            )
            .bind(message)
            .bind(room)
            .bind(user_id)
            .execute(bus.inner.db.writer())
            .await?;

            if result.rows_affected() == 0 {
                return Ok(None);
            }

            let seq_row: Option<(i64,)> = db::sqlx::query_as(
                "SELECT seq FROM room_message WHERE id = $1",
            )
            .bind(message)
            .fetch_optional(bus.inner.db.reader())
            .await?;

            let data = star::MessageStarredService {
                room: room_info.clone(),
                message_id: message,
                message_seq: seq_row.map(|r| r.0).unwrap_or(0),
                starred_by: user_info,
                starred_at: Utc::now(),
            };
            bus.emit_to_user(user_id, "message.starred", &data).await?;
            Ok(Some(WsOutEvent::MessageStarred {
                room: room_info,
                data,
            }))
        } else {
            let result = db::sqlx::query(
                "DELETE FROM message_star WHERE message = $1 AND \"user\" = $2",
            )
            .bind(message)
            .bind(user_id)
            .execute(bus.inner.db.writer())
            .await?;

            if result.rows_affected() == 0 {
                return Ok(None);
            }

            let data = star::MessageUnstarredService {
                room: room_info.clone(),
                message_id: message,
                unstarred_by: user_info,
                unstarred_at: Utc::now(),
            };
            bus.emit_to_user(user_id, "message.unstarred", &data)
                .await?;
            Ok(Some(WsOutEvent::MessageUnstarred {
                room: room_info,
                data,
            }))
        }
    }
    pub(super) async fn starred_list(
        bus: &ChannelBus,
        user_id: Uuid,
        room: Option<Uuid>,
        limit: Option<u64>,
    ) -> ChannelResult<Option<WsOutEvent>> {
        let limit = limit.unwrap_or(50).min(100) as i64;

        let rows = if let Some(room_id) = room {
            Self::ensure_room_access(bus, user_id, room_id).await?;
            db::sqlx::query_as::<_, (Uuid, Uuid, i64, String, String, Uuid, chrono::DateTime<Utc>, chrono::DateTime<Utc>)>(
                "SELECT ms.id, rm.id, rm.seq, rm.content, rm.content_type, rm.author, ms.created_at, rm.created_at \
                 FROM message_star ms \
                 JOIN room_message rm ON rm.id = ms.message \
                 WHERE ms.\"user\" = $1 AND ms.room = $2 AND rm.deleted_at IS NULL \
                 ORDER BY ms.created_at DESC LIMIT $3",
            )
            .bind(user_id)
            .bind(room_id)
            .bind(limit)
            .fetch_all(bus.inner.db.reader())
            .await?
        } else {
            db::sqlx::query_as::<_, (Uuid, Uuid, i64, String, String, Uuid, chrono::DateTime<Utc>, chrono::DateTime<Utc>)>(
                "SELECT ms.id, rm.id, rm.seq, rm.content, rm.content_type, rm.author, ms.created_at, rm.created_at \
                 FROM message_star ms \
                 JOIN room_message rm ON rm.id = ms.message \
                 WHERE ms.\"user\" = $1 AND rm.deleted_at IS NULL \
                 ORDER BY ms.created_at DESC LIMIT $2",
            )
            .bind(user_id)
            .bind(limit)
            .fetch_all(bus.inner.db.reader())
            .await?
        };

        let author_ids: Vec<Uuid> = rows.iter().map(|r| r.5).collect();
        let user_map = bus.lookup_users(&author_ids).await.unwrap_or_default();

        let mut entries = Vec::with_capacity(rows.len());
        for (
            _star_id,
            msg_id,
            seq,
            content,
            content_type,
            author_id,
            starred_at,
            sent_at,
        ) in rows
        {
            let msg_room_row: Option<(Uuid,)> = db::sqlx::query_as(
                "SELECT room FROM room_message WHERE id = $1",
            )
            .bind(msg_id)
            .fetch_optional(bus.inner.db.reader())
            .await?;
            let msg_room_id = msg_room_row.map(|r| r.0).unwrap_or(Uuid::nil());
            let room_info = bus
                .lookup_room(msg_room_id)
                .await
                .unwrap_or_else(|_| RoomInfo::unknown(msg_room_id));
            let sender = user_map
                .get(&author_id)
                .cloned()
                .unwrap_or_else(|| UserInfo::unknown(author_id));

            entries.push(star::StarredMessageEntry {
                message_id: msg_id,
                room: room_info,
                seq,
                content,
                content_type,
                sender,
                starred_at,
                sent_at,
            });
        }

        Ok(Some(WsOutEvent::StarredList { data: entries }))
    }
}
