use uuid::Uuid;

use crate::event::{RoomInfo, UserInfo, article};
use crate::http::handler::WsHandler;
use crate::http::out_event::WsOutEvent;
use crate::{ChannelBus, ChannelError, ChannelResult};

impl WsHandler {
    pub(super) async fn article_create(
        bus: &ChannelBus,
        user_id: Uuid,
        channel: Uuid,
        title: String,
        cover_url: Option<String>,
        content: String,
        content_type: Option<String>,
        summary: Option<String>,
        tags: Option<Vec<String>>,
        status: Option<String>,
    ) -> ChannelResult<Option<WsOutEvent>> {
        Self::ensure_room_access(bus, user_id, channel).await?;

        let ctype = content_type.unwrap_or_else(|| "markdown".to_string());
        let st = status.unwrap_or_else(|| "published".to_string());
        let tag_list = tags.unwrap_or_default();

        let row = db::sqlx::query_as::<_, model::channel::ChannelArticleModel>(
            "INSERT INTO channel_article \
             (channel, author, title, cover_url, content, content_type, summary, tags, status, created_at, updated_at) \
             VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, now(), now()) \
             RETURNING id, channel, author, title, cover_url, content, content_type, summary, \
                       tags, is_pinned, view_count, like_count, comment_count, status, \
                       created_at, updated_at, deleted_at",
        )
        .bind(channel)
        .bind(user_id)
        .bind(&title)
        .bind(&cover_url)
        .bind(&content)
        .bind(&ctype)
        .bind(&summary)
        .bind(&tag_list)
        .bind(&st)
        .fetch_one(bus.inner.db.writer())
        .await?;

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

        let item = article_item_from_model(&row, &author);
        let data = article::ArticleCreatedService {
            article: item,
            channel: room.clone(),
            author: author.clone(),
        };

        bus.publish_room_event(channel, "article.created", &data).await?;

        Ok(Some(WsOutEvent::ArticleCreated {
            room,
            data,
        }))
    }

    pub(super) async fn article_update(
        bus: &ChannelBus,
        user_id: Uuid,
        article_id: Uuid,
        title: Option<String>,
        cover_url: Option<String>,
        content: Option<String>,
        content_type: Option<String>,
        summary: Option<String>,
        tags: Option<Vec<String>>,
        is_pinned: Option<bool>,
        status: Option<String>,
    ) -> ChannelResult<Option<WsOutEvent>> {
        let old = db::sqlx::query_as::<_, model::channel::ChannelArticleModel>(
            "SELECT id, channel, author, title, cover_url, content, content_type, summary, \
                    tags, is_pinned, view_count, like_count, comment_count, status, \
                    created_at, updated_at, deleted_at \
             FROM channel_article WHERE id = $1 AND deleted_at IS NULL",
        )
        .bind(article_id)
        .fetch_optional(bus.inner.db.reader())
        .await?
        .ok_or(ChannelError::RoomNotFound)?;

        if old.author != user_id {
            return Err(ChannelError::AccessDenied);
        }
        Self::ensure_room_access(bus, user_id, old.channel).await?;

        let new_title = title.unwrap_or(old.title.clone());
        let new_cover = cover_url.or(old.cover_url.clone());
        let new_content = content.unwrap_or(old.content.clone());
        let new_ctype = content_type.unwrap_or(old.content_type.clone());
        let new_summary = summary.or(old.summary.clone());
        let new_tags = tags.unwrap_or(old.tags.clone());
        let new_pinned = is_pinned.unwrap_or(old.is_pinned);
        let new_status = status.unwrap_or(old.status.clone());

        let row = db::sqlx::query_as::<_, model::channel::ChannelArticleModel>(
            "UPDATE channel_article \
             SET title = $2, cover_url = $3, content = $4, content_type = $5, \
                 summary = $6, tags = $7, is_pinned = $8, status = $9, updated_at = now() \
             WHERE id = $1 AND deleted_at IS NULL \
             RETURNING id, channel, author, title, cover_url, content, content_type, summary, \
                       tags, is_pinned, view_count, like_count, comment_count, status, \
                       created_at, updated_at, deleted_at",
        )
        .bind(article_id)
        .bind(&new_title)
        .bind(&new_cover)
        .bind(&new_content)
        .bind(&new_ctype)
        .bind(&new_summary)
        .bind(&new_tags)
        .bind(new_pinned)
        .bind(&new_status)
        .fetch_one(bus.inner.db.writer())
        .await?;

        let author = bus
            .lookup_user(row.author)
            .await
            .unwrap_or_else(|_| UserInfo::unknown(row.author));
        let room = bus
            .lookup_room(row.channel)
            .await
            .unwrap_or_else(|_| RoomInfo::unknown(row.channel));

        let item = article_item_from_model(&row, &author);
        let data = article::ArticleUpdatedService {
            article: item,
            channel: room.clone(),
        };

        bus.publish_room_event(row.channel, "article.updated", &data).await?;

        Ok(Some(WsOutEvent::ArticleUpdated {
            room,
            data,
        }))
    }

    pub(super) async fn article_delete(
        bus: &ChannelBus,
        user_id: Uuid,
        article_id: Uuid,
    ) -> ChannelResult<Option<WsOutEvent>> {
        let old = db::sqlx::query_as::<_, model::channel::ChannelArticleModel>(
            "SELECT id, channel, author, title, cover_url, content, content_type, summary, \
                    tags, is_pinned, view_count, like_count, comment_count, status, \
                    created_at, updated_at, deleted_at \
             FROM channel_article WHERE id = $1 AND deleted_at IS NULL",
        )
        .bind(article_id)
        .fetch_optional(bus.inner.db.reader())
        .await?
        .ok_or(ChannelError::RoomNotFound)?;

        if old.author != user_id {
            return Err(ChannelError::AccessDenied);
        }

        db::sqlx::query(
            "UPDATE channel_article SET deleted_at = now(), updated_at = now() \
             WHERE id = $1 AND deleted_at IS NULL",
        )
        .bind(article_id)
        .execute(bus.inner.db.writer())
        .await?;

        let deleted_by = bus
            .lookup_user(user_id)
            .await
            .unwrap_or_else(|_| UserInfo::unknown(user_id));
        let room = bus
            .lookup_room(old.channel)
            .await
            .unwrap_or_else(|_| RoomInfo::unknown(old.channel));

        let data = article::ArticleDeletedService {
            article_id,
            channel: room.clone(),
            deleted_by: deleted_by.clone(),
        };

        bus.publish_room_event(old.channel, "article.deleted", &data).await?;

        Ok(Some(WsOutEvent::ArticleDeleted {
            room,
            data,
        }))
    }

    pub(super) async fn article_list(
        bus: &ChannelBus,
        user_id: Uuid,
        channel: Uuid,
        before: Option<Uuid>,
        limit: Option<i64>,
    ) -> ChannelResult<Option<WsOutEvent>> {
        Self::ensure_room_access(bus, user_id, channel).await?;

        let lim = limit.unwrap_or(20).min(50);
        let rows = if let Some(cursor) = before {
            // Cursor-based pagination: get articles older than the cursor
            db::sqlx::query_as::<_, model::channel::ChannelArticleCard>(
                "SELECT id, channel, author, title, cover_url, summary, tags, \
                        like_count, comment_count, view_count, created_at \
                 FROM channel_article \
                 WHERE channel = $1 AND deleted_at IS NULL \
                   AND created_at < (SELECT created_at FROM channel_article WHERE id = $2) \
                 ORDER BY is_pinned DESC, created_at DESC \
                 LIMIT $3",
            )
            .bind(channel)
            .bind(cursor)
            .bind(lim)
            .fetch_all(bus.inner.db.reader())
            .await?
        } else {
            db::sqlx::query_as::<_, model::channel::ChannelArticleCard>(
                "SELECT id, channel, author, title, cover_url, summary, tags, \
                        like_count, comment_count, view_count, created_at \
                 FROM channel_article \
                 WHERE channel = $1 AND deleted_at IS NULL \
                 ORDER BY is_pinned DESC, created_at DESC \
                 LIMIT $2",
            )
            .bind(channel)
            .bind(lim)
            .fetch_all(bus.inner.db.reader())
            .await?
        };

        let total: (i64,) =
            db::sqlx::query_as("SELECT COUNT(*) FROM channel_article WHERE channel = $1 AND deleted_at IS NULL")
                .bind(channel)
                .fetch_one(bus.inner.db.reader())
                .await?;

        let author_ids: Vec<Uuid> = rows.iter().map(|r| r.author).collect();
        let users = bus.lookup_users(&author_ids).await?;
        let articles: Vec<article::ArticleItem> = rows
            .iter()
            .map(|r| {
                let author = users
                    .get(&r.author)
                    .cloned()
                    .unwrap_or_else(|| UserInfo::unknown(r.author));
                article_item_from_card(r, &author)
            })
            .collect();

        let data = article::ArticleListService {
            has_more: articles.len() as i64 >= lim,
            articles,
            total: total.0,
        };

        Ok(Some(WsOutEvent::ArticleList { data }))
    }

    pub(super) async fn article_get(
        bus: &ChannelBus,
        user_id: Uuid,
        article_id: Uuid,
    ) -> ChannelResult<Option<WsOutEvent>> {
        let row = db::sqlx::query_as::<_, model::channel::ChannelArticleModel>(
            "SELECT id, channel, author, title, cover_url, content, content_type, summary, \
                    tags, is_pinned, view_count, like_count, comment_count, status, \
                    created_at, updated_at, deleted_at \
             FROM channel_article WHERE id = $1 AND deleted_at IS NULL",
        )
        .bind(article_id)
        .fetch_optional(bus.inner.db.reader())
        .await?
        .ok_or(ChannelError::RoomNotFound)?;

        Self::ensure_room_access(bus, user_id, row.channel).await?;

        // Increment view count (fire-and-forget)
        let _ = db::sqlx::query(
            "UPDATE channel_article SET view_count = view_count + 1 WHERE id = $1",
        )
        .bind(article_id)
        .execute(bus.inner.db.writer())
        .await;

        let author = bus
            .lookup_user(row.author)
            .await
            .unwrap_or_else(|_| UserInfo::unknown(row.author));

        let card = article_item_from_model(&row, &author);
        let detail = article::ArticleDetail {
            content: row.content,
            card,
        };

        Ok(Some(WsOutEvent::ArticleDetail { data: detail }))
    }

    pub(super) async fn article_like(
        bus: &ChannelBus,
        user_id: Uuid,
        article_id: Uuid,
        like: bool,
    ) -> ChannelResult<Option<WsOutEvent>> {
        let art = db::sqlx::query_as::<_, model::channel::ChannelArticleModel>(
            "SELECT id, channel, author, title, cover_url, content, content_type, summary, \
                    tags, is_pinned, view_count, like_count, comment_count, status, \
                    created_at, updated_at, deleted_at \
             FROM channel_article WHERE id = $1 AND deleted_at IS NULL",
        )
        .bind(article_id)
        .fetch_optional(bus.inner.db.reader())
        .await?
        .ok_or(ChannelError::RoomNotFound)?;

        Self::ensure_room_access(bus, user_id, art.channel).await?;

        let new_count = if like {
            let affected = db::sqlx::query(
                "INSERT INTO channel_article_like (article, \"user\") VALUES ($1, $2) ON CONFLICT DO NOTHING",
            )
            .bind(article_id)
            .bind(user_id)
            .execute(bus.inner.db.writer())
            .await?;
            if affected.rows_affected() == 0 {
                return Ok(None);
            }
            let row: (i64,) = db::sqlx::query_as(
                "UPDATE channel_article SET like_count = like_count + 1 WHERE id = $1 RETURNING like_count",
            )
            .bind(article_id)
            .fetch_one(bus.inner.db.writer())
            .await?;
            row.0
        } else {
            let affected = db::sqlx::query(
                "DELETE FROM channel_article_like WHERE article = $1 AND \"user\" = $2",
            )
            .bind(article_id)
            .bind(user_id)
            .execute(bus.inner.db.writer())
            .await?;
            if affected.rows_affected() == 0 {
                return Ok(None);
            }
            let row: (i64,) = db::sqlx::query_as(
                "UPDATE channel_article SET like_count = GREATEST(like_count - 1, 0) WHERE id = $1 RETURNING like_count",
            )
            .bind(article_id)
            .fetch_one(bus.inner.db.writer())
            .await?;
            row.0
        };

        let user = bus
            .lookup_user(user_id)
            .await
            .unwrap_or_else(|_| UserInfo::unknown(user_id));
        let room = bus
            .lookup_room(art.channel)
            .await
            .unwrap_or_else(|_| RoomInfo::unknown(art.channel));

        if like {
            let data = article::ArticleLikedService {
                article_id,
                channel: room.clone(),
                user,
                like_count: new_count,
            };
            bus.publish_room_event(art.channel, "article.liked", &data).await?;
            Ok(Some(WsOutEvent::ArticleLiked { room, data }))
        } else {
            let data = article::ArticleUnlikedService {
                article_id,
                channel: room.clone(),
                user,
                like_count: new_count,
            };
            bus.publish_room_event(art.channel, "article.unliked", &data).await?;
            Ok(Some(WsOutEvent::ArticleUnliked { room, data }))
        }
    }

    pub(super) async fn article_comment_create(
        bus: &ChannelBus,
        user_id: Uuid,
        article_id: Uuid,
        content: String,
        parent: Option<Uuid>,
    ) -> ChannelResult<Option<WsOutEvent>> {
        let art = db::sqlx::query_as::<_, model::channel::ChannelArticleModel>(
            "SELECT id, channel, author, title, cover_url, content, content_type, summary, \
                    tags, is_pinned, view_count, like_count, comment_count, status, \
                    created_at, updated_at, deleted_at \
             FROM channel_article WHERE id = $1 AND deleted_at IS NULL",
        )
        .bind(article_id)
        .fetch_optional(bus.inner.db.reader())
        .await?
        .ok_or(ChannelError::RoomNotFound)?;

        Self::ensure_room_access(bus, user_id, art.channel).await?;

        let row = db::sqlx::query_as::<_, model::channel::ArticleCommentModel>(
            "INSERT INTO channel_article_comment (article, author, parent, content, created_at, updated_at) \
             VALUES ($1, $2, $3, $4, now(), now()) \
             RETURNING id, article, author, parent, content, created_at, updated_at, deleted_at",
        )
        .bind(article_id)
        .bind(user_id)
        .bind(parent)
        .bind(&content)
        .fetch_one(bus.inner.db.writer())
        .await?;

        let count_row: (i64,) = db::sqlx::query_as(
            "UPDATE channel_article SET comment_count = comment_count + 1 WHERE id = $1 RETURNING comment_count",
        )
        .bind(article_id)
        .fetch_one(bus.inner.db.writer())
        .await?;

        let author = bus
            .lookup_user(user_id)
            .await
            .unwrap_or_else(|_| UserInfo::unknown(user_id));
        let room = bus
            .lookup_room(art.channel)
            .await
            .unwrap_or_else(|_| RoomInfo::unknown(art.channel));

        let item = model::channel::ArticleCommentItem {
            id: row.id,
            article: row.article,
            parent: row.parent,
            content: row.content,
            author: row.author,
            created_at: row.created_at,
            updated_at: row.updated_at,
        };

        let data = article::ArticleCommentCreatedService {
            comment: item,
            channel: room.clone(),
            author,
            comment_count: count_row.0,
        };

        bus.publish_room_event(art.channel, "article.comment.created", &data).await?;

        Ok(Some(WsOutEvent::ArticleCommentCreated { room, data }))
    }

    pub(super) async fn article_comment_list(
        bus: &ChannelBus,
        user_id: Uuid,
        article_id: Uuid,
        before: Option<Uuid>,
        limit: Option<i64>,
    ) -> ChannelResult<Option<WsOutEvent>> {
        let art = db::sqlx::query_as::<_, (Uuid,)>(
            "SELECT channel FROM channel_article WHERE id = $1 AND deleted_at IS NULL",
        )
        .bind(article_id)
        .fetch_optional(bus.inner.db.reader())
        .await?
        .ok_or(ChannelError::RoomNotFound)?;

        Self::ensure_room_access(bus, user_id, art.0).await?;

        let lim = limit.unwrap_or(30).min(50);
        let rows = if let Some(cursor) = before {
            db::sqlx::query_as::<_, model::channel::ArticleCommentModel>(
                "SELECT id, article, author, parent, content, created_at, updated_at, deleted_at \
                 FROM channel_article_comment \
                 WHERE article = $1 AND deleted_at IS NULL \
                   AND created_at > (SELECT created_at FROM channel_article_comment WHERE id = $2) \
                 ORDER BY created_at ASC \
                 LIMIT $3",
            )
            .bind(article_id)
            .bind(cursor)
            .bind(lim)
            .fetch_all(bus.inner.db.reader())
            .await?
        } else {
            db::sqlx::query_as::<_, model::channel::ArticleCommentModel>(
                "SELECT id, article, author, parent, content, created_at, updated_at, deleted_at \
                 FROM channel_article_comment \
                 WHERE article = $1 AND deleted_at IS NULL \
                 ORDER BY created_at ASC \
                 LIMIT $2",
            )
            .bind(article_id)
            .bind(lim)
            .fetch_all(bus.inner.db.reader())
            .await?
        };

        let total: (i64,) = db::sqlx::query_as(
            "SELECT COUNT(*) FROM channel_article_comment WHERE article = $1 AND deleted_at IS NULL",
        )
        .bind(article_id)
        .fetch_one(bus.inner.db.reader())
        .await?;

        let comments: Vec<model::channel::ArticleCommentItem> = rows
            .into_iter()
            .map(|r| model::channel::ArticleCommentItem {
                id: r.id,
                article: r.article,
                parent: r.parent,
                content: r.content,
                author: r.author,
                created_at: r.created_at,
                updated_at: r.updated_at,
            })
            .collect();

        let data = article::ArticleCommentListService {
            comments,
            total: total.0,
        };

        Ok(Some(WsOutEvent::ArticleCommentList { data }))
    }

    pub(super) async fn article_comment_delete(
        bus: &ChannelBus,
        user_id: Uuid,
        comment_id: Uuid,
    ) -> ChannelResult<Option<WsOutEvent>> {
        let cmt = db::sqlx::query_as::<_, model::channel::ArticleCommentModel>(
            "SELECT id, article, author, parent, content, created_at, updated_at, deleted_at \
             FROM channel_article_comment WHERE id = $1 AND deleted_at IS NULL",
        )
        .bind(comment_id)
        .fetch_optional(bus.inner.db.reader())
        .await?
        .ok_or(ChannelError::RoomNotFound)?;

        let art = db::sqlx::query_as::<_, model::channel::ChannelArticleModel>(
            "SELECT id, channel, author FROM channel_article WHERE id = $1 AND deleted_at IS NULL",
        )
        .bind(cmt.article)
        .fetch_optional(bus.inner.db.reader())
        .await?
        .ok_or(ChannelError::RoomNotFound)?;

        // Auth: article author can delete any comment; comment author can delete own
        if art.author != user_id && cmt.author != user_id {
            return Err(ChannelError::AccessDenied);
        }

        db::sqlx::query(
            "UPDATE channel_article_comment SET deleted_at = now(), updated_at = now() \
             WHERE id = $1 AND deleted_at IS NULL",
        )
        .bind(comment_id)
        .execute(bus.inner.db.writer())
        .await?;

        let count_row: (i64,) = db::sqlx::query_as(
            "UPDATE channel_article SET comment_count = GREATEST(comment_count - 1, 0) \
             WHERE id = $1 RETURNING comment_count",
        )
        .bind(cmt.article)
        .fetch_one(bus.inner.db.writer())
        .await?;

        let deleted_by = bus
            .lookup_user(user_id)
            .await
            .unwrap_or_else(|_| UserInfo::unknown(user_id));
        let room = bus
            .lookup_room(art.channel)
            .await
            .unwrap_or_else(|_| RoomInfo::unknown(art.channel));

        let data = article::ArticleCommentDeletedService {
            comment_id,
            article_id: cmt.article,
            channel: room.clone(),
            deleted_by,
            comment_count: count_row.0,
        };

        bus.publish_room_event(art.channel, "article.comment.deleted", &data).await?;
        Ok(Some(WsOutEvent::ArticleCommentDeleted { room, data }))
    }

    pub(super) async fn article_liked_users(
        bus: &ChannelBus,
        user_id: Uuid,
        article_id: Uuid,
        before: Option<Uuid>,
        limit: Option<i64>,
    ) -> ChannelResult<Option<WsOutEvent>> {
        let art = db::sqlx::query_as::<_, (Uuid,)>(
            "SELECT channel FROM channel_article WHERE id = $1 AND deleted_at IS NULL",
        )
        .bind(article_id)
        .fetch_optional(bus.inner.db.reader())
        .await?
        .ok_or(ChannelError::RoomNotFound)?;

        Self::ensure_room_access(bus, user_id, art.0).await?;

        let lim = limit.unwrap_or(30).min(50);
        let rows: Vec<(Uuid,)> = if let Some(cursor) = before {
            db::sqlx::query_as(
                "SELECT l.\"user\" FROM channel_article_like l \
                 WHERE l.article = $1 \
                   AND l.created_at < (SELECT created_at FROM channel_article_like WHERE article = $1 AND \"user\" = $2) \
                 ORDER BY l.created_at DESC LIMIT $3",
            )
            .bind(article_id)
            .bind(cursor)
            .bind(lim)
            .fetch_all(bus.inner.db.reader())
            .await?
        } else {
            db::sqlx::query_as(
                "SELECT \"user\" FROM channel_article_like \
                 WHERE article = $1 ORDER BY created_at DESC LIMIT $2",
            )
            .bind(article_id)
            .bind(lim)
            .fetch_all(bus.inner.db.reader())
            .await?
        };

        let total: (i64,) = db::sqlx::query_as(
            "SELECT COUNT(*) FROM channel_article_like WHERE article = $1",
        )
        .bind(article_id)
        .fetch_one(bus.inner.db.reader())
        .await?;

        let user_ids: Vec<Uuid> = rows.iter().map(|r| r.0).collect();
        let users_map = bus.lookup_users(&user_ids).await?;
        let users: Vec<UserInfo> = user_ids
            .iter()
            .map(|id| users_map.get(id).cloned().unwrap_or_else(|| UserInfo::unknown(*id)))
            .collect();

        Ok(Some(WsOutEvent::ArticleLikedUsers {
            data: article::ArticleLikedUsersService {
                article_id,
                users,
                total: total.0,
            },
        }))
    }
}

fn article_item_from_model(
    m: &model::channel::ChannelArticleModel,
    author: &UserInfo,
) -> article::ArticleItem {
    article::ArticleItem {
        id: m.id,
        channel: m.channel,
        author: author.clone(),
        title: m.title.clone(),
        cover_url: m.cover_url.clone(),
        summary: m.summary.clone(),
        tags: m.tags.clone(),
        like_count: m.like_count,
        comment_count: m.comment_count,
        view_count: m.view_count,
        is_pinned: m.is_pinned,
        content_type: m.content_type.clone(),
        status: m.status.clone(),
        created_at: m.created_at,
        updated_at: m.updated_at,
    }
}

fn article_item_from_card(
    m: &model::channel::ChannelArticleCard,
    author: &UserInfo,
) -> article::ArticleItem {
    article::ArticleItem {
        id: m.id,
        channel: m.channel,
        author: author.clone(),
        title: m.title.clone(),
        cover_url: m.cover_url.clone(),
        summary: m.summary.clone(),
        tags: m.tags.clone(),
        like_count: m.like_count,
        comment_count: m.comment_count,
        view_count: m.view_count,
        is_pinned: false, // card doesn't carry is_pinned
        content_type: String::new(),
        status: String::new(),
        created_at: m.created_at,
        updated_at: m.created_at, // card only has created_at
    }
}
