use cache::AppCache;
use db::{AppDatabase, sqlx};
use model::channel::RoomMessageModel;
use serde::Serialize;
use uuid::Uuid;

use crate::{ChannelBusConfig, ChannelResult};

pub(crate) const RM_COLUMNS: &str = "id, room, seq, thread, parent, author, content, content_type, pinned, \
     system_type, metadata, edited_at, created_at, updated_at, deleted_at";

pub(crate) fn room_socket_name(room: Uuid) -> String {
    format!("room:{room}")
}

pub(crate) fn user_rooms_cache_key(user: Uuid) -> String {
    format!("channel:user:{user}:rooms")
}
#[derive(Debug, Serialize)]
pub struct RoomListItem {
    pub id: Uuid,
    pub name: String,
    pub topic: Option<String>,
    /// Maps to DB `room_type` column. Serialized as `room_type` for frontend compat.
    pub room_type: String,
    pub is_private: bool,
    pub ai_enabled: bool,
    pub category: Option<Uuid>,
    pub workspace_id: Uuid,
}
#[derive(Debug, Serialize)]
pub struct CategoryListItem {
    pub id: Uuid,
    pub name: String,
    pub position: i32,
}
pub async fn user_rooms_for_api(
    db: &AppDatabase,
    cache: &AppCache,
    config: &ChannelBusConfig,
    user: Uuid,
) -> ChannelResult<Vec<RoomListItem>> {
    let room_ids = user_rooms(db, cache, config, user).await?;
    if room_ids.is_empty() {
        return Ok(Vec::new());
    }

    let rows = sqlx::query_as::<
        _,
        (
            Uuid,
            String,
            Option<String>,
            String,
            bool,
            bool,
            Option<Uuid>,
            Uuid,
        ),
    >(
        "SELECT id, name, topic, room_type, is_private, ai_enabled, parent, wk \
         FROM room \
         WHERE id = ANY($1) AND deleted_at IS NULL AND is_archived = false \
         ORDER BY name",
    )
    .bind(&room_ids)
    .fetch_all(db.reader())
    .await?;

    Ok(rows
        .into_iter()
        .map(
            |(
                id,
                name,
                topic,
                room_type,
                is_private,
                ai_enabled,
                category,
                workspace_id,
            )| RoomListItem {
                id,
                name,
                topic,
                room_type,
                is_private,
                ai_enabled,
                category,
                workspace_id,
            },
        )
        .collect())
}
pub async fn user_categories_for_api(
    db: &AppDatabase,
    cache: &AppCache,
    config: &ChannelBusConfig,
    user: Uuid,
) -> ChannelResult<Vec<CategoryListItem>> {
    let room_ids = user_rooms(db, cache, config, user).await?;
    if room_ids.is_empty() {
        return Ok(Vec::new());
    }
    let wk_rows = sqlx::query_as::<_, (Uuid,)>(
        "SELECT DISTINCT wk FROM room WHERE id = ANY($1) AND deleted_at IS NULL",
    )
    .bind(&room_ids)
    .fetch_all(db.reader())
    .await?;

    let wk_ids: Vec<Uuid> = wk_rows.into_iter().map(|r| r.0).collect();
    if wk_ids.is_empty() {
        return Ok(Vec::new());
    }

    let rows = sqlx::query_as::<_, (Uuid, String, i32)>(
        "SELECT id, name, position FROM room_category WHERE wk = ANY($1) ORDER BY position, name",
    )
    .bind(&wk_ids)
    .fetch_all(db.reader())
    .await?;

    Ok(rows
        .into_iter()
        .map(|(id, name, position)| CategoryListItem { id, name, position })
        .collect())
}

pub(crate) async fn user_rooms(
    db: &AppDatabase,
    cache: &AppCache,
    config: &ChannelBusConfig,
    user: Uuid,
) -> ChannelResult<Vec<Uuid>> {
    let key = user_rooms_cache_key(user);
    if let Some(rooms) = cache.get::<Vec<Uuid>>(&key).await? {
        return Ok(rooms);
    }

    let rooms = load_user_rooms(db, user).await?;
    cache_set_with_ttl(cache, &key, &rooms, config.room_cache_ttl_hint).await?;
    Ok(rooms)
}

pub(crate) async fn refresh_user_rooms_cache(
    db: &AppDatabase,
    cache: &AppCache,
    config: &ChannelBusConfig,
    user: Uuid,
) -> ChannelResult<Vec<Uuid>> {
    let key = user_rooms_cache_key(user);
    cache.remove(&key).await?;
    let rooms = load_user_rooms(db, user).await?;
    cache_set_with_ttl(cache, &key, &rooms, config.room_cache_ttl_hint).await?;
    Ok(rooms)
}

async fn cache_set_with_ttl<T>(
    cache: &AppCache,
    key: &str,
    value: &T,
    ttl: Option<std::time::Duration>,
) -> ChannelResult<()>
where
    T: serde::Serialize + serde::de::DeserializeOwned,
{
    cache.set(key, value).await?;
    if let Some(ttl) = ttl {
        if let Some(cluster) = &cache.cluster {
            cluster.expire(key, ttl).await?;
        }
    }
    Ok(())
}

pub(crate) async fn active_workspace_users(
    db: &AppDatabase,
    wk: Uuid,
) -> ChannelResult<Vec<Uuid>> {
    let rows = sqlx::query_as::<_, (Uuid,)>(
        "SELECT \"user\" FROM wk_member WHERE wk = $1 AND leave_at IS NULL",
    )
    .bind(wk)
    .fetch_all(db.reader())
    .await?;
    Ok(rows.into_iter().map(|row| row.0).collect())
}

pub(crate) async fn room_workspace(
    db: &AppDatabase,
    room: Uuid,
) -> ChannelResult<Option<Uuid>> {
    let row = sqlx::query_as::<_, (Uuid,)>("SELECT wk FROM room WHERE id = $1")
        .bind(room)
        .fetch_optional(db.reader())
        .await?;
    Ok(row.map(|row| row.0))
}

pub(crate) async fn catchup_messages(
    db: &AppDatabase,
    config: &ChannelBusConfig,
    room: Uuid,
    after_seq: i64,
) -> ChannelResult<Vec<RoomMessageModel>> {
    let rows = sqlx::query_as::<_, RoomMessageModel>(db::sqlx::AssertSqlSafe(
        format!(
            "SELECT {RM_COLUMNS} FROM room_message \
             WHERE room = $1 AND seq > $2 AND deleted_at IS NULL \
             ORDER BY seq ASC \
             LIMIT $3"
        ),
    ))
    .bind(room)
    .bind(after_seq)
    .bind(config.catchup_limit)
    .fetch_all(db.reader())
    .await?;
    Ok(rows)
}

async fn load_user_rooms(
    db: &AppDatabase,
    user: Uuid,
) -> ChannelResult<Vec<Uuid>> {
    let rows = sqlx::query_as::<_, (Uuid,)>(
        "SELECT r.id \
         FROM room r \
         INNER JOIN wk_member wm ON wm.wk = r.wk \
         WHERE wm.\"user\" = $1 \
           AND wm.leave_at IS NULL \
           AND r.deleted_at IS NULL \
           AND r.is_archived = false \
           AND (r.is_private = false \
                OR EXISTS ( \
                    SELECT 1 FROM room_permission_overwrite po \
                    WHERE po.room = r.id AND po.target_id = $1 \
                )) \
         ORDER BY r.id",
    )
    .bind(user)
    .fetch_all(db.reader())
    .await?;
    Ok(rows.into_iter().map(|row| row.0).collect())
}
