//! WebSocket handler for channel presence.
//!
//! Handles real-time position updates, emotion changes, and member synchronization.

use axum::{
    extract::{
        FromRef, Path, State,
        ws::{CloseFrame, Message, WebSocket, WebSocketUpgrade},
    },
    response::IntoResponse,
};
use dashmap::DashMap;
use futures::{SinkExt, StreamExt};
use sqlx::PgPool;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::{broadcast, mpsc};
use uuid::Uuid;

use chattyness_db::{
    models::{AvatarRenderData, ChannelMemberWithAvatar, EmotionState, User},
    queries::{avatars, channel_members, loose_props, realms, scenes},
    ws_messages::{ClientMessage, DisconnectReason, ServerMessage, WsConfig},
};
use chattyness_error::AppError;
use chattyness_shared::WebSocketConfig;

/// Close code for scene change (custom code).
const SCENE_CHANGE_CLOSE_CODE: u16 = 4000;

use crate::auth::AuthUser;

/// Channel state for broadcasting updates.
pub struct ChannelState {
    /// Broadcast sender for this channel.
    tx: broadcast::Sender<ServerMessage>,
}

/// Connection info for a connected user.
#[derive(Clone)]
pub struct UserConnection {
    /// Direct message sender for this user.
    pub direct_tx: mpsc::Sender<ServerMessage>,
    /// Realm the user is in.
    pub realm_id: Uuid,
    /// Channel (scene) the user is in.
    pub channel_id: Uuid,
    /// User's display name.
    pub display_name: String,
}

/// Global state for all WebSocket connections.
pub struct WebSocketState {
    /// Map of channel_id -> ChannelState.
    channels: DashMap<Uuid, Arc<ChannelState>>,
    /// Map of user_id -> UserConnection for direct message routing.
    users: DashMap<Uuid, UserConnection>,
}

impl Default for WebSocketState {
    fn default() -> Self {
        Self::new()
    }
}

impl WebSocketState {
    /// Create a new WebSocket state.
    pub fn new() -> Self {
        Self {
            channels: DashMap::new(),
            users: DashMap::new(),
        }
    }

    /// Get or create a channel state.
    fn get_or_create_channel(&self, channel_id: Uuid) -> Arc<ChannelState> {
        self.channels
            .entry(channel_id)
            .or_insert_with(|| {
                let (tx, _) = broadcast::channel(256);
                Arc::new(ChannelState { tx })
            })
            .clone()
    }

    /// Register a user connection for direct messaging.
    pub fn register_user(
        &self,
        user_id: Uuid,
        direct_tx: mpsc::Sender<ServerMessage>,
        realm_id: Uuid,
        channel_id: Uuid,
        display_name: String,
    ) {
        self.users.insert(
            user_id,
            UserConnection {
                direct_tx,
                realm_id,
                channel_id,
                display_name,
            },
        );
    }

    /// Unregister a user connection.
    pub fn unregister_user(&self, user_id: Uuid) {
        self.users.remove(&user_id);
    }

    /// Find a user by display name within a realm.
    pub fn find_user_by_display_name(
        &self,
        realm_id: Uuid,
        display_name: &str,
    ) -> Option<(Uuid, UserConnection)> {
        for entry in self.users.iter() {
            let (user_id, conn) = entry.pair();
            if conn.realm_id == realm_id && conn.display_name.eq_ignore_ascii_case(display_name) {
                return Some((*user_id, conn.clone()));
            }
        }
        None
    }

    /// Get a user's connection info.
    pub fn get_user(&self, user_id: Uuid) -> Option<UserConnection> {
        self.users.get(&user_id).map(|r| r.clone())
    }
}

/// WebSocket upgrade handler.
///
/// GET /api/realms/{slug}/channels/{channel_id}/ws
pub async fn ws_handler<S>(
    Path((slug, channel_id)): Path<(String, Uuid)>,
    auth_result: Result<AuthUser, crate::auth::AuthError>,
    State(pool): State<PgPool>,
    State(ws_state): State<Arc<WebSocketState>>,
    State(ws_config): State<WebSocketConfig>,
    ws: WebSocketUpgrade,
) -> Result<impl IntoResponse, AppError>
where
    S: Send + Sync,
    PgPool: FromRef<S>,
    Arc<WebSocketState>: FromRef<S>,
    WebSocketConfig: FromRef<S>,
{
    // Log auth result before checking
    #[cfg(debug_assertions)]
    tracing::debug!(
        "[WS] Connection attempt to {}/channels/{} - auth: {:?}",
        slug,
        channel_id,
        auth_result
            .as_ref()
            .map(|a| a.0.id)
            .map_err(|e| format!("{:?}", e))
    );

    let AuthUser(user) = auth_result.map_err(|e| {
        tracing::warn!(
            "[WS] Auth failed for {}/channels/{}: {:?}",
            slug,
            channel_id,
            e
        );
        AppError::from(e)
    })?;

    // Verify realm exists
    let realm = realms::get_realm_by_slug(&pool, &slug)
        .await?
        .ok_or_else(|| AppError::NotFound(format!("Realm '{}' not found", slug)))?;

    // Verify scene exists and belongs to this realm
    // Note: Using scene_id as channel_id since channel_members uses scenes directly
    let scene = scenes::get_scene_by_id(&pool, channel_id)
        .await?
        .ok_or_else(|| AppError::NotFound("Scene not found".to_string()))?;

    if scene.realm_id != realm.id {
        return Err(AppError::NotFound(
            "Scene not found in this realm".to_string(),
        ));
    }

    #[cfg(debug_assertions)]
    tracing::debug!(
        "[WS] Upgrading connection for user {} to channel {}",
        user.id,
        channel_id
    );

    Ok(ws.on_upgrade(move |socket| {
        handle_socket(
            socket, user, channel_id, realm.id, pool, ws_state, ws_config,
        )
    }))
}

/// Set RLS context on a database connection.
async fn set_rls_user_id(
    conn: &mut sqlx::pool::PoolConnection<sqlx::Postgres>,
    user_id: Uuid,
) -> Result<(), sqlx::Error> {
    sqlx::query("SELECT public.set_current_user_id($1)")
        .bind(user_id)
        .execute(&mut **conn)
        .await?;
    Ok(())
}

/// Handle an active WebSocket connection.
async fn handle_socket(
    socket: WebSocket,
    user: User,
    channel_id: Uuid,
    realm_id: Uuid,
    pool: PgPool,
    ws_state: Arc<WebSocketState>,
    ws_config: WebSocketConfig,
) {
    tracing::info!(
        "[WS] handle_socket started for user {} channel {} realm {}",
        user.id,
        channel_id,
        realm_id
    );

    // Acquire a dedicated connection for setup operations
    let mut conn = match pool.acquire().await {
        Ok(conn) => conn,
        Err(e) => {
            tracing::error!("[WS] Failed to acquire DB connection: {:?}", e);
            return;
        }
    };

    // Set RLS context on this dedicated connection
    if let Err(e) = set_rls_user_id(&mut conn, user.id).await {
        tracing::error!(
            "[WS] Failed to set RLS context for user {}: {:?}",
            user.id,
            e
        );
        return;
    }
    tracing::info!("[WS] RLS context set on dedicated connection");

    let channel_state = ws_state.get_or_create_channel(channel_id);
    let mut rx = channel_state.tx.subscribe();

    let (mut sender, mut receiver) = socket.split();

    // Ensure active avatar
    tracing::info!("[WS] Ensuring active avatar...");
    if let Err(e) = channel_members::ensure_active_avatar(&mut *conn, user.id, realm_id).await {
        tracing::error!("[WS] Failed to ensure avatar for user {}: {:?}", user.id, e);
        return;
    }
    tracing::info!("[WS] Avatar ensured");

    // Join the channel
    tracing::info!("[WS] Joining channel...");
    if let Err(e) = channel_members::join_channel(&mut *conn, channel_id, user.id).await {
        tracing::error!(
            "[WS] Failed to join channel {} for user {}: {:?}",
            channel_id,
            user.id,
            e
        );
        return;
    }
    tracing::info!("[WS] Channel joined");

    // Get initial state
    let members = match get_members_with_avatars(&mut conn, channel_id, realm_id).await {
        Ok(m) => m,
        Err(e) => {
            tracing::error!("[WS] Failed to get members: {:?}", e);
            let _ = channel_members::leave_channel(&mut *conn, channel_id, user.id).await;
            return;
        }
    };

    let member = match channel_members::get_channel_member(
        &mut *conn, channel_id, user.id, realm_id,
    )
    .await
    {
        Ok(Some(m)) => m,
        Ok(None) => {
            tracing::error!("[WS] Failed to get member info for user {}", user.id);
            let _ = channel_members::leave_channel(&mut *conn, channel_id, user.id).await;
            return;
        }
        Err(e) => {
            tracing::error!("[WS] Error getting member info: {:?}", e);
            let _ = channel_members::leave_channel(&mut *conn, channel_id, user.id).await;
            return;
        }
    };

    // Send welcome message with config
    let welcome = ServerMessage::Welcome {
        member: member.clone(),
        members,
        config: WsConfig {
            ping_interval_secs: ws_config.client_ping_interval_secs,
        },
    };
    if let Ok(json) = serde_json::to_string(&welcome) {
        #[cfg(debug_assertions)]
        tracing::debug!("[WS->Client] {}", json);
        if sender.send(Message::Text(json.into())).await.is_err() {
            let _ = channel_members::leave_channel(&mut *conn, channel_id, user.id).await;
            return;
        }
    }

    // Send loose props sync
    match loose_props::list_channel_loose_props(&mut *conn, channel_id).await {
        Ok(props) => {
            let props_sync = ServerMessage::LoosePropsSync { props };
            if let Ok(json) = serde_json::to_string(&props_sync) {
                #[cfg(debug_assertions)]
                tracing::debug!("[WS->Client] {}", json);
                if sender.send(Message::Text(json.into())).await.is_err() {
                    let _ = channel_members::leave_channel(&mut *conn, channel_id, user.id).await;
                    return;
                }
            }
        }
        Err(e) => {
            tracing::warn!("[WS] Failed to get loose props: {:?}", e);
        }
    }

    // Save member display_name for user registration (before member is moved)
    let member_display_name = member.display_name.clone();

    // Broadcast join to others
    let avatar = avatars::get_avatar_with_paths_conn(&mut *conn, user.id, realm_id)
        .await
        .ok()
        .flatten()
        .map(|a| a.to_render_data())
        .unwrap_or_default();
    let join_msg = ServerMessage::MemberJoined {
        member: ChannelMemberWithAvatar { member, avatar },
    };
    let _ = channel_state.tx.send(join_msg);

    let user_id = user.id;
    let tx = channel_state.tx.clone();

    // Acquire a second dedicated connection for the receive task
    // This connection needs its own RLS context
    let mut recv_conn = match pool.acquire().await {
        Ok(c) => c,
        Err(e) => {
            tracing::error!("[WS] Failed to acquire recv connection: {:?}", e);
            let _ = channel_members::leave_channel(&mut *conn, channel_id, user_id).await;
            return;
        }
    };
    if let Err(e) = set_rls_user_id(&mut recv_conn, user_id).await {
        tracing::error!("[WS] Failed to set RLS on recv connection: {:?}", e);
        let _ = channel_members::leave_channel(&mut *conn, channel_id, user_id).await;
        return;
    }

    // Drop the setup connection - we'll use recv_conn for the receive task
    // and pool for cleanup (leave_channel needs user_id match anyway)
    drop(conn);

    // Channel for sending direct messages (Pong, whispers) to client
    let (direct_tx, mut direct_rx) = mpsc::channel::<ServerMessage>(16);

    // Register user for direct message routing
    ws_state.register_user(
        user_id,
        direct_tx.clone(),
        realm_id,
        channel_id,
        member_display_name,
    );

    // Clone ws_state for use in recv_task
    let ws_state_for_recv = ws_state.clone();

    // Create recv timeout from config
    let recv_timeout = Duration::from_secs(ws_config.recv_timeout_secs);

    // Spawn task to handle incoming messages from client
    let recv_task = tokio::spawn(async move {
        let ws_state = ws_state_for_recv;
        let mut disconnect_reason = DisconnectReason::Graceful;

        loop {
            // Use timeout to detect connection loss
            let msg_result = tokio::time::timeout(recv_timeout, receiver.next()).await;

            match msg_result {
                Ok(Some(Ok(msg))) => {
                    match msg {
                        Message::Text(text) => {
                            #[cfg(debug_assertions)]
                            tracing::debug!("[WS<-Client] {}", text);

                            let Ok(client_msg) = serde_json::from_str::<ClientMessage>(&text)
                            else {
                                continue;
                            };

                            match client_msg {
                                ClientMessage::UpdatePosition { x, y } => {
                                    if let Err(e) = channel_members::update_position(
                                        &mut *recv_conn,
                                        channel_id,
                                        user_id,
                                        x,
                                        y,
                                    )
                                    .await
                                    {
                                        #[cfg(debug_assertions)]
                                        tracing::error!("[WS] Position update failed: {:?}", e);
                                        continue;
                                    }
                                    let _ = tx.send(ServerMessage::PositionUpdated {
                                        user_id: Some(user_id),
                                        guest_session_id: None,
                                        x,
                                        y,
                                    });
                                }
                                ClientMessage::UpdateEmotion { emotion } => {
                                    // Parse emotion name to EmotionState
                                    let emotion_state = match emotion.parse::<EmotionState>() {
                                        Ok(e) => e,
                                        Err(_) => {
                                            #[cfg(debug_assertions)]
                                            tracing::warn!(
                                                "[WS] Invalid emotion name: {}",
                                                emotion
                                            );
                                            continue;
                                        }
                                    };
                                    let emotion_layer = match avatars::set_emotion(
                                        &mut *recv_conn,
                                        user_id,
                                        realm_id,
                                        emotion_state,
                                    )
                                    .await
                                    {
                                        Ok(layer) => layer,
                                        Err(e) => {
                                            #[cfg(debug_assertions)]
                                            tracing::error!("[WS] Emotion update failed: {:?}", e);
                                            continue;
                                        }
                                    };
                                    let _ = tx.send(ServerMessage::EmotionUpdated {
                                        user_id: Some(user_id),
                                        guest_session_id: None,
                                        emotion,
                                        emotion_layer,
                                    });
                                }
                                ClientMessage::Ping => {
                                    // Update last_moved_at to keep member alive for cleanup
                                    let _ = channel_members::touch_member(
                                        &mut *recv_conn,
                                        channel_id,
                                        user_id,
                                    )
                                    .await;
                                    // Respond with pong directly (not broadcast)
                                    let _ = direct_tx.send(ServerMessage::Pong).await;
                                }
                                ClientMessage::SendChatMessage {
                                    content,
                                    target_display_name,
                                } => {
                                    // Validate message
                                    if content.is_empty() || content.len() > 500 {
                                        continue;
                                    }

                                    // Get member's current position and emotion
                                    let member_info = channel_members::get_channel_member(
                                        &mut *recv_conn,
                                        channel_id,
                                        user_id,
                                        realm_id,
                                    )
                                    .await;

                                    if let Ok(Some(member)) = member_info {
                                        // Convert emotion index to name
                                        let emotion_name =
                                            EmotionState::from_index(member.current_emotion as u8)
                                                .map(|e| e.to_string())
                                                .unwrap_or_else(|| "neutral".to_string());

                                        // Handle whisper (direct message) vs broadcast
                                        if let Some(target_name) = target_display_name {
                                            // Whisper: send directly to target user
                                            if let Some((_target_user_id, target_conn)) = ws_state
                                                .find_user_by_display_name(realm_id, &target_name)
                                            {
                                                // Determine if same scene
                                                let is_same_scene =
                                                    target_conn.channel_id == channel_id;

                                                let msg = ServerMessage::ChatMessageReceived {
                                                    message_id: Uuid::new_v4(),
                                                    user_id: Some(user_id),
                                                    guest_session_id: None,
                                                    display_name: member.display_name.clone(),
                                                    content: content.clone(),
                                                    emotion: emotion_name.clone(),
                                                    x: member.position_x,
                                                    y: member.position_y,
                                                    timestamp: chrono::Utc::now()
                                                        .timestamp_millis(),
                                                    is_whisper: true,
                                                    is_same_scene,
                                                };

                                                // Send to target user
                                                let _ =
                                                    target_conn.direct_tx.send(msg.clone()).await;

                                                // Also send back to sender (so they see their own whisper)
                                                // For sender, is_same_scene is always true (they see it as a bubble)
                                                let sender_msg =
                                                    ServerMessage::ChatMessageReceived {
                                                        message_id: Uuid::new_v4(),
                                                        user_id: Some(user_id),
                                                        guest_session_id: None,
                                                        display_name: member.display_name.clone(),
                                                        content,
                                                        emotion: emotion_name,
                                                        x: member.position_x,
                                                        y: member.position_y,
                                                        timestamp: chrono::Utc::now()
                                                            .timestamp_millis(),
                                                        is_whisper: true,
                                                        is_same_scene: true, // Sender always sees as bubble
                                                    };
                                                let _ = direct_tx.send(sender_msg).await;

                                                #[cfg(debug_assertions)]
                                                tracing::debug!(
                                                    "[WS] Whisper from {} to {} (same_scene={})",
                                                    member.display_name,
                                                    target_name,
                                                    is_same_scene
                                                );
                                            } else {
                                                // Target user not found - send error
                                                let _ = direct_tx.send(ServerMessage::Error {
                                                    code: "WHISPER_TARGET_NOT_FOUND".to_string(),
                                                    message: format!("User '{}' is not online or not in this realm", target_name),
                                                }).await;
                                            }
                                        } else {
                                            // Broadcast: send to all users in the channel
                                            let msg = ServerMessage::ChatMessageReceived {
                                                message_id: Uuid::new_v4(),
                                                user_id: Some(user_id),
                                                guest_session_id: None,
                                                display_name: member.display_name.clone(),
                                                content,
                                                emotion: emotion_name,
                                                x: member.position_x,
                                                y: member.position_y,
                                                timestamp: chrono::Utc::now().timestamp_millis(),
                                                is_whisper: false,
                                                is_same_scene: true,
                                            };
                                            let _ = tx.send(msg);
                                        }
                                    }
                                }
                                ClientMessage::DropProp { inventory_item_id } => {
                                    // Get user's current position for random offset
                                    let member_info = channel_members::get_channel_member(
                                        &mut *recv_conn,
                                        channel_id,
                                        user_id,
                                        realm_id,
                                    )
                                    .await;

                                    if let Ok(Some(member)) = member_info {
                                        // Generate random offset (within ~50 pixels)
                                        let offset_x = (rand::random::<f64>() - 0.5) * 100.0;
                                        let offset_y = (rand::random::<f64>() - 0.5) * 100.0;
                                        let pos_x = member.position_x + offset_x;
                                        let pos_y = member.position_y + offset_y;

                                        match loose_props::drop_prop_to_canvas(
                                            &mut *recv_conn,
                                            inventory_item_id,
                                            user_id,
                                            channel_id,
                                            pos_x,
                                            pos_y,
                                        )
                                        .await
                                        {
                                            Ok(prop) => {
                                                #[cfg(debug_assertions)]
                                                tracing::debug!(
                                                    "[WS] User {} dropped prop {} at ({}, {})",
                                                    user_id,
                                                    prop.id,
                                                    pos_x,
                                                    pos_y
                                                );
                                                let _ =
                                                    tx.send(ServerMessage::PropDropped { prop });
                                            }
                                            Err(e) => {
                                                tracing::error!("[WS] Drop prop failed: {:?}", e);
                                                let (code, message) = match &e {
                                                    chattyness_error::AppError::Forbidden(msg) => (
                                                        "PROP_NOT_DROPPABLE".to_string(),
                                                        msg.clone(),
                                                    ),
                                                    chattyness_error::AppError::NotFound(msg) => {
                                                        ("PROP_NOT_FOUND".to_string(), msg.clone())
                                                    }
                                                    _ => (
                                                        "DROP_FAILED".to_string(),
                                                        format!("{:?}", e),
                                                    ),
                                                };
                                                let _ =
                                                    tx.send(ServerMessage::Error { code, message });
                                            }
                                        }
                                    }
                                }
                                ClientMessage::PickUpProp { loose_prop_id } => {
                                    match loose_props::pick_up_loose_prop(
                                        &mut *recv_conn,
                                        loose_prop_id,
                                        user_id,
                                    )
                                    .await
                                    {
                                        Ok(_inventory_item) => {
                                            #[cfg(debug_assertions)]
                                            tracing::debug!(
                                                "[WS] User {} picked up prop {}",
                                                user_id,
                                                loose_prop_id
                                            );
                                            let _ = tx.send(ServerMessage::PropPickedUp {
                                                prop_id: loose_prop_id,
                                                picked_up_by_user_id: Some(user_id),
                                                picked_up_by_guest_id: None,
                                            });
                                        }
                                        Err(e) => {
                                            tracing::error!("[WS] Pick up prop failed: {:?}", e);
                                            let _ = tx.send(ServerMessage::Error {
                                                code: "PICKUP_FAILED".to_string(),
                                                message: format!("{:?}", e),
                                            });
                                        }
                                    }
                                }
                                ClientMessage::SyncAvatar => {
                                    // Fetch current avatar from database and broadcast to channel
                                    match avatars::get_avatar_with_paths_conn(
                                        &mut *recv_conn,
                                        user_id,
                                        realm_id,
                                    )
                                    .await
                                    {
                                        Ok(Some(avatar_with_paths)) => {
                                            let render_data = avatar_with_paths.to_render_data();
                                            #[cfg(debug_assertions)]
                                            tracing::debug!(
                                                "[WS] User {} syncing avatar to channel",
                                                user_id
                                            );
                                            let _ = tx.send(ServerMessage::AvatarUpdated {
                                                user_id: Some(user_id),
                                                guest_session_id: None,
                                                avatar: render_data,
                                            });
                                        }
                                        Ok(None) => {
                                            #[cfg(debug_assertions)]
                                            tracing::warn!(
                                                "[WS] No avatar found for user {} to sync",
                                                user_id
                                            );
                                        }
                                        Err(e) => {
                                            tracing::error!("[WS] Avatar sync failed: {:?}", e);
                                        }
                                    }
                                }
                            }
                        }
                        Message::Close(close_frame) => {
                            // Check close code for scene change
                            if let Some(CloseFrame { code, .. }) = close_frame {
                                if code == SCENE_CHANGE_CLOSE_CODE {
                                    disconnect_reason = DisconnectReason::SceneChange;
                                } else {
                                    disconnect_reason = DisconnectReason::Graceful;
                                }
                            }
                            break;
                        }
                        _ => {
                            // Ignore binary, ping, pong messages
                        }
                    }
                }
                Ok(Some(Err(e))) => {
                    // WebSocket error
                    tracing::warn!("[WS] Connection error: {:?}", e);
                    disconnect_reason = DisconnectReason::Timeout;
                    break;
                }
                Ok(None) => {
                    // Stream ended gracefully
                    break;
                }
                Err(_) => {
                    // Timeout elapsed - connection likely lost
                    tracing::info!("[WS] Connection timeout for user {}", user_id);
                    disconnect_reason = DisconnectReason::Timeout;
                    break;
                }
            }
        }
        // Return the connection and disconnect reason for cleanup
        (recv_conn, disconnect_reason)
    });

    // Spawn task to forward broadcasts and direct messages to this client
    let send_task = tokio::spawn(async move {
        loop {
            tokio::select! {
                // Handle broadcast messages
                Ok(msg) = rx.recv() => {
                    if let Ok(json) = serde_json::to_string(&msg) {
                        #[cfg(debug_assertions)]
                        tracing::debug!("[WS->Client] {}", json);
                        if sender.send(Message::Text(json.into())).await.is_err() {
                            break;
                        }
                    }
                }
                // Handle direct messages (Pong)
                Some(msg) = direct_rx.recv() => {
                    if let Ok(json) = serde_json::to_string(&msg) {
                        #[cfg(debug_assertions)]
                        tracing::debug!("[WS->Client] {}", json);
                        if sender.send(Message::Text(json.into())).await.is_err() {
                            break;
                        }
                    }
                }
                else => break
            }
        }
    });

    // Wait for either task to complete, track disconnect reason
    let disconnect_reason = tokio::select! {
        recv_result = recv_task => {
            // recv_task finished, get connection and reason back for cleanup
            if let Ok((mut cleanup_conn, reason)) = recv_result {
                let _ = channel_members::leave_channel(&mut *cleanup_conn, channel_id, user_id).await;
                reason
            } else {
                // Task panicked, use pool (RLS may fail but try anyway)
                let _ = channel_members::leave_channel(&pool, channel_id, user_id).await;
                DisconnectReason::Timeout
            }
        }
        _ = send_task => {
            // send_task finished first (likely client disconnect), need to acquire a new connection for cleanup
            if let Ok(mut cleanup_conn) = pool.acquire().await {
                let _ = set_rls_user_id(&mut cleanup_conn, user_id).await;
                let _ = channel_members::leave_channel(&mut *cleanup_conn, channel_id, user_id).await;
            }
            DisconnectReason::Graceful
        }
    };

    // Unregister user from direct message routing
    ws_state.unregister_user(user_id);

    tracing::info!(
        "[WS] User {} disconnected from channel {} (reason: {:?})",
        user_id,
        channel_id,
        disconnect_reason
    );

    // Broadcast departure with reason
    let _ = channel_state.tx.send(ServerMessage::MemberLeft {
        user_id: Some(user_id),
        guest_session_id: None,
        reason: disconnect_reason,
    });
}

/// Helper: Get all channel members with their avatar render data.
async fn get_members_with_avatars(
    conn: &mut sqlx::pool::PoolConnection<sqlx::Postgres>,
    channel_id: Uuid,
    realm_id: Uuid,
) -> Result<Vec<ChannelMemberWithAvatar>, AppError> {
    // Get members first
    let members = channel_members::get_channel_members(&mut **conn, channel_id, realm_id).await?;

    // Fetch avatar data for each member using full avatar with paths
    // This avoids the CASE statement approach and handles all emotions correctly
    let mut result = Vec::with_capacity(members.len());
    for member in members {
        let avatar = if let Some(user_id) = member.user_id {
            // Get full avatar and convert to render data for current emotion
            avatars::get_avatar_with_paths_conn(&mut **conn, user_id, realm_id)
                .await
                .ok()
                .flatten()
                .map(|a| a.to_render_data())
                .unwrap_or_default()
        } else {
            // Guest users don't have avatars
            AvatarRenderData::default()
        };
        result.push(ChannelMemberWithAvatar { member, avatar });
    }

    Ok(result)
}
