//! WebSocket client for channel presence.
//!
//! Provides a Leptos hook to manage WebSocket connections for real-time
//! position updates, emotion changes, and member synchronization.

use leptos::prelude::*;
use leptos::reactive::owner::LocalStorage;

use chattyness_db::models::{ChannelMemberWithAvatar, EmotionState, LooseProp};
use chattyness_db::ws_messages::{ClientMessage, ServerMessage};

use super::chat_types::ChatMessage;

/// WebSocket connection state.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum WsState {
    /// Attempting to connect.
    Connecting,
    /// Connected and ready.
    Connected,
    /// Disconnected (not connected).
    Disconnected,
    /// Connection error occurred.
    Error,
}

/// Sender function type for WebSocket messages.
pub type WsSender = Box<dyn Fn(ClientMessage)>;

/// Local stored value type for the sender (non-Send, WASM-compatible).
pub type WsSenderStorage = StoredValue<Option<WsSender>, LocalStorage>;

/// Hook to manage WebSocket connection for a channel.
///
/// Returns a tuple of:
/// - `Signal<WsState>` - The current connection state
/// - `WsSenderStorage` - A stored sender function to send messages
#[cfg(feature = "hydrate")]
pub fn use_channel_websocket(
    realm_slug: Signal<String>,
    channel_id: Signal<Option<uuid::Uuid>>,
    on_members_update: Callback<Vec<ChannelMemberWithAvatar>>,
    on_chat_message: Callback<ChatMessage>,
    on_loose_props_sync: Callback<Vec<LooseProp>>,
    on_prop_dropped: Callback<LooseProp>,
    on_prop_picked_up: Callback<uuid::Uuid>,
) -> (Signal<WsState>, WsSenderStorage) {
    use std::cell::RefCell;
    use std::rc::Rc;
    use wasm_bindgen::{closure::Closure, JsCast};
    use web_sys::{CloseEvent, ErrorEvent, MessageEvent, WebSocket};

    let (ws_state, set_ws_state) = signal(WsState::Disconnected);
    let ws_ref: Rc<RefCell<Option<WebSocket>>> = Rc::new(RefCell::new(None));
    let members: Rc<RefCell<Vec<ChannelMemberWithAvatar>>> = Rc::new(RefCell::new(Vec::new()));

    // Create a stored sender function (using new_local for WASM single-threaded environment)
    let ws_ref_for_send = ws_ref.clone();
    let sender: WsSenderStorage = StoredValue::new_local(Some(Box::new(
        move |msg: ClientMessage| {
            if let Some(ws) = ws_ref_for_send.borrow().as_ref() {
                if ws.ready_state() == WebSocket::OPEN {
                    if let Ok(json) = serde_json::to_string(&msg) {
                        #[cfg(debug_assertions)]
                        web_sys::console::log_1(&format!("[WS->Server] {}", json).into());
                        let _ = ws.send_with_str(&json);
                    }
                }
            }
        },
    )));

    // Effect to manage WebSocket lifecycle
    let ws_ref_clone = ws_ref.clone();
    let members_clone = members.clone();

    Effect::new(move |_| {
        let slug = realm_slug.get();
        let ch_id = channel_id.get();

        // Cleanup previous connection
        if let Some(old_ws) = ws_ref_clone.borrow_mut().take() {
            let _ = old_ws.close();
        }

        let Some(ch_id) = ch_id else {
            set_ws_state.set(WsState::Disconnected);
            return;
        };

        if slug.is_empty() {
            set_ws_state.set(WsState::Disconnected);
            return;
        }

        // Construct WebSocket URL
        let window = web_sys::window().unwrap();
        let location = window.location();
        let protocol = if location.protocol().unwrap_or_default() == "https:" {
            "wss:"
        } else {
            "ws:"
        };
        let host = location.host().unwrap_or_default();
        let url = format!(
            "{}//{}/api/realms/{}/channels/{}/ws",
            protocol, host, slug, ch_id
        );

        #[cfg(debug_assertions)]
        web_sys::console::log_1(&format!("[WS] Connecting to: {}", url).into());

        set_ws_state.set(WsState::Connecting);

        let ws = match WebSocket::new(&url) {
            Ok(ws) => ws,
            Err(e) => {
                #[cfg(debug_assertions)]
                web_sys::console::error_1(&format!("[WS] Failed to create: {:?}", e).into());
                set_ws_state.set(WsState::Error);
                return;
            }
        };

        // onopen
        let set_ws_state_open = set_ws_state;
        let onopen = Closure::wrap(Box::new(move |_: web_sys::Event| {
            #[cfg(debug_assertions)]
            web_sys::console::log_1(&"[WS] Connected".into());
            set_ws_state_open.set(WsState::Connected);
        }) as Box<dyn FnMut(web_sys::Event)>);
        ws.set_onopen(Some(onopen.as_ref().unchecked_ref()));
        onopen.forget();

        // onmessage
        let members_for_msg = members_clone.clone();
        let on_members_update_clone = on_members_update.clone();
        let on_chat_message_clone = on_chat_message.clone();
        let on_loose_props_sync_clone = on_loose_props_sync.clone();
        let on_prop_dropped_clone = on_prop_dropped.clone();
        let on_prop_picked_up_clone = on_prop_picked_up.clone();
        let onmessage = Closure::wrap(Box::new(move |e: MessageEvent| {
            if let Ok(text) = e.data().dyn_into::<js_sys::JsString>() {
                let text: String = text.into();
                #[cfg(debug_assertions)]
                web_sys::console::log_1(&format!("[WS<-Server] {}", text).into());

                if let Ok(msg) = serde_json::from_str::<ServerMessage>(&text) {
                    handle_server_message(
                        msg,
                        &members_for_msg,
                        &on_members_update_clone,
                        &on_chat_message_clone,
                        &on_loose_props_sync_clone,
                        &on_prop_dropped_clone,
                        &on_prop_picked_up_clone,
                    );
                }
            }
        }) as Box<dyn FnMut(MessageEvent)>);
        ws.set_onmessage(Some(onmessage.as_ref().unchecked_ref()));
        onmessage.forget();

        // onerror
        let set_ws_state_err = set_ws_state;
        let onerror = Closure::wrap(Box::new(move |e: ErrorEvent| {
            #[cfg(debug_assertions)]
            web_sys::console::error_1(&format!("[WS] Error: {:?}", e.message()).into());
            set_ws_state_err.set(WsState::Error);
        }) as Box<dyn FnMut(ErrorEvent)>);
        ws.set_onerror(Some(onerror.as_ref().unchecked_ref()));
        onerror.forget();

        // onclose
        let set_ws_state_close = set_ws_state;
        let onclose = Closure::wrap(Box::new(move |e: CloseEvent| {
            #[cfg(debug_assertions)]
            web_sys::console::log_1(
                &format!("[WS] Closed: code={}, reason={}", e.code(), e.reason()).into(),
            );
            set_ws_state_close.set(WsState::Disconnected);
        }) as Box<dyn FnMut(CloseEvent)>);
        ws.set_onclose(Some(onclose.as_ref().unchecked_ref()));
        onclose.forget();

        *ws_ref_clone.borrow_mut() = Some(ws);
    });

    (Signal::derive(move || ws_state.get()), sender)
}

/// Handle a message received from the server.
#[cfg(feature = "hydrate")]
fn handle_server_message(
    msg: ServerMessage,
    members: &std::rc::Rc<std::cell::RefCell<Vec<ChannelMemberWithAvatar>>>,
    on_update: &Callback<Vec<ChannelMemberWithAvatar>>,
    on_chat_message: &Callback<ChatMessage>,
    on_loose_props_sync: &Callback<Vec<LooseProp>>,
    on_prop_dropped: &Callback<LooseProp>,
    on_prop_picked_up: &Callback<uuid::Uuid>,
) {
    let mut members_vec = members.borrow_mut();

    match msg {
        ServerMessage::Welcome {
            member: _,
            members: initial_members,
        } => {
            *members_vec = initial_members;
            on_update.run(members_vec.clone());
        }
        ServerMessage::MemberJoined { member } => {
            // Remove if exists (rejoin case), then add
            members_vec.retain(|m| {
                m.member.user_id != member.member.user_id
                    || m.member.guest_session_id != member.member.guest_session_id
            });
            members_vec.push(member);
            on_update.run(members_vec.clone());
        }
        ServerMessage::MemberLeft {
            user_id,
            guest_session_id,
        } => {
            members_vec.retain(|m| {
                m.member.user_id != user_id || m.member.guest_session_id != guest_session_id
            });
            on_update.run(members_vec.clone());
        }
        ServerMessage::PositionUpdated {
            user_id,
            guest_session_id,
            x,
            y,
        } => {
            if let Some(m) = members_vec.iter_mut().find(|m| {
                m.member.user_id == user_id && m.member.guest_session_id == guest_session_id
            }) {
                m.member.position_x = x;
                m.member.position_y = y;
            }
            on_update.run(members_vec.clone());
        }
        ServerMessage::EmotionUpdated {
            user_id,
            guest_session_id,
            emotion,
            emotion_layer,
        } => {
            if let Some(m) = members_vec.iter_mut().find(|m| {
                m.member.user_id == user_id && m.member.guest_session_id == guest_session_id
            }) {
                // Convert emotion name to index for internal state
                m.member.current_emotion = emotion
                    .parse::<EmotionState>()
                    .map(|e| e.to_index() as i16)
                    .unwrap_or(0);
                m.avatar.emotion_layer = emotion_layer;
            }
            on_update.run(members_vec.clone());
        }
        ServerMessage::Pong => {
            // Heartbeat acknowledged - nothing to do
        }
        ServerMessage::Error { code, message } => {
            #[cfg(debug_assertions)]
            web_sys::console::error_1(&format!("[WS] Server error: {} - {}", code, message).into());
        }
        ServerMessage::ChatMessageReceived {
            message_id,
            user_id,
            guest_session_id,
            display_name,
            content,
            emotion,
            x,
            y,
            timestamp,
        } => {
            let chat_msg = ChatMessage {
                message_id,
                user_id,
                guest_session_id,
                display_name,
                content,
                emotion,
                x,
                y,
                timestamp,
            };
            on_chat_message.run(chat_msg);
        }
        ServerMessage::LoosePropsSync { props } => {
            on_loose_props_sync.run(props);
        }
        ServerMessage::PropDropped { prop } => {
            on_prop_dropped.run(prop);
        }
        ServerMessage::PropPickedUp { prop_id, .. } => {
            on_prop_picked_up.run(prop_id);
        }
        ServerMessage::PropExpired { prop_id } => {
            // Treat expired props the same as picked up (remove from display)
            on_prop_picked_up.run(prop_id);
        }
    }
}

/// Stub implementation for SSR (server-side rendering).
#[cfg(not(feature = "hydrate"))]
pub fn use_channel_websocket(
    _realm_slug: Signal<String>,
    _channel_id: Signal<Option<uuid::Uuid>>,
    _on_members_update: Callback<Vec<ChannelMemberWithAvatar>>,
    _on_chat_message: Callback<ChatMessage>,
    _on_loose_props_sync: Callback<Vec<LooseProp>>,
    _on_prop_dropped: Callback<LooseProp>,
    _on_prop_picked_up: Callback<uuid::Uuid>,
) -> (Signal<WsState>, WsSenderStorage) {
    let (ws_state, _) = signal(WsState::Disconnected);
    let sender: WsSenderStorage = StoredValue::new_local(None);
    (Signal::derive(move || ws_state.get()), sender)
}
