//! 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;

#[cfg(feature = "hydrate")]
use chattyness_db::models::EmotionState;
use chattyness_db::models::{ChannelMemberWithAvatar, LooseProp};
use chattyness_db::ws_messages::ClientMessage;
#[cfg(feature = "hydrate")]
use chattyness_db::ws_messages::{DisconnectReason, ServerMessage};

use super::chat_types::ChatMessage;

/// Close code for scene change (must match server constant).
pub const SCENE_CHANGE_CLOSE_CODE: u16 = 4000;

/// Duration for fade-out animation in milliseconds.
pub const FADE_DURATION_MS: i64 = 5000;

/// A member that is currently fading out after a timeout disconnect.
#[derive(Clone, Debug)]
pub struct FadingMember {
    /// The member data.
    pub member: ChannelMemberWithAvatar,
    /// Timestamp when the fade started (milliseconds since epoch).
    pub fade_start: i64,
    /// Duration of the fade in milliseconds.
    pub fade_duration: i64,
}

/// 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>;

/// Information about the current channel member (received on Welcome).
#[derive(Clone, Debug)]
pub struct ChannelMemberInfo {
    /// The user's user_id (if authenticated user).
    pub user_id: Option<uuid::Uuid>,
    /// The user's guest_session_id (if guest).
    pub guest_session_id: Option<uuid::Uuid>,
    /// The user's display name.
    pub display_name: String,
    /// Whether this user is a guest (has the 'guest' tag).
    pub is_guest: bool,
}

/// WebSocket error info for UI display.
#[derive(Clone, Debug)]
pub struct WsError {
    /// Error code from server.
    pub code: String,
    /// Human-readable error message.
    pub message: String,
}

/// 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>>,
    reconnect_trigger: Signal<u32>,
    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>,
    on_member_fading: Callback<FadingMember>,
    on_welcome: Option<Callback<ChannelMemberInfo>>,
    on_error: Option<Callback<WsError>>,
) -> (Signal<WsState>, WsSenderStorage) {
    use std::cell::RefCell;
    use std::rc::Rc;
    use wasm_bindgen::{JsCast, closure::Closure};
    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();
        // Track reconnect_trigger to force reconnection when it changes
        let _trigger = reconnect_trigger.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 on_member_fading_clone = on_member_fading.clone();
        let on_welcome_clone = on_welcome.clone();
        let on_error_clone = on_error.clone();
        // For starting heartbeat on Welcome
        let ws_ref_for_heartbeat = ws_ref.clone();
        let heartbeat_started: Rc<RefCell<bool>> = Rc::new(RefCell::new(false));
        let heartbeat_started_clone = heartbeat_started.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) {
                    // Check for Welcome message to start heartbeat with server-provided config
                    if let ServerMessage::Welcome {
                        ref config,
                        ref member,
                        ..
                    } = msg
                    {
                        if !*heartbeat_started_clone.borrow() {
                            *heartbeat_started_clone.borrow_mut() = true;
                            let ping_interval_ms = config.ping_interval_secs * 1000;
                            let ws_ref_ping = ws_ref_for_heartbeat.clone();
                            #[cfg(debug_assertions)]
                            web_sys::console::log_1(
                                &format!(
                                    "[WS] Starting heartbeat with interval {}ms",
                                    ping_interval_ms
                                )
                                .into(),
                            );
                            let heartbeat = gloo_timers::callback::Interval::new(
                                ping_interval_ms as u32,
                                move || {
                                    if let Some(ws) = ws_ref_ping.borrow().as_ref() {
                                        if ws.ready_state() == WebSocket::OPEN {
                                            if let Ok(json) =
                                                serde_json::to_string(&ClientMessage::Ping)
                                            {
                                                let _ = ws.send_with_str(&json);
                                            }
                                        }
                                    }
                                },
                            );
                            std::mem::forget(heartbeat);
                        }
                        // Call on_welcome callback with current user info
                        if let Some(ref callback) = on_welcome_clone {
                            let info = ChannelMemberInfo {
                                user_id: member.user_id,
                                guest_session_id: member.guest_session_id,
                                display_name: member.display_name.clone(),
                                is_guest: member.is_guest,
                            };
                            callback.run(info);
                        }
                    }
                    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,
                        &on_member_fading_clone,
                        &on_error_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>,
    on_member_fading: &Callback<FadingMember>,
    on_error: &Option<Callback<WsError>>,
) {
    let mut members_vec = members.borrow_mut();

    match msg {
        ServerMessage::Welcome {
            member: _,
            members: initial_members,
            config: _, // Config is handled in the caller for heartbeat setup
        } => {
            *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,
            reason,
        } => {
            // Find the member before removing
            let leaving_member = members_vec
                .iter()
                .find(|m| {
                    m.member.user_id == user_id && m.member.guest_session_id == guest_session_id
                })
                .cloned();

            // Always remove from active members list
            members_vec.retain(|m| {
                m.member.user_id != user_id || m.member.guest_session_id != guest_session_id
            });
            on_update.run(members_vec.clone());

            // For timeout disconnects, trigger fading animation
            if reason == DisconnectReason::Timeout {
                if let Some(member) = leaving_member {
                    let fading = FadingMember {
                        member,
                        fade_start: js_sys::Date::now() as i64,
                        fade_duration: FADE_DURATION_MS,
                    };
                    on_member_fading.run(fading);
                }
            }
        }
        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 } => {
            // Always log errors to console (not just debug mode)
            web_sys::console::error_1(&format!("[WS] Server error: {} - {}", code, message).into());
            // Call error callback if provided
            if let Some(callback) = on_error {
                callback.run(WsError { code, message });
            }
        }
        ServerMessage::ChatMessageReceived {
            message_id,
            user_id,
            guest_session_id,
            display_name,
            content,
            emotion,
            x,
            y,
            timestamp,
            is_whisper,
            is_same_scene,
        } => {
            let chat_msg = ChatMessage {
                message_id,
                user_id,
                guest_session_id,
                display_name,
                content,
                emotion,
                x,
                y,
                timestamp,
                is_whisper,
                is_same_scene,
            };
            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);
        }
        ServerMessage::AvatarUpdated {
            user_id,
            guest_session_id,
            avatar,
        } => {
            // Find member and update their avatar layers
            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.avatar.skin_layer = avatar.skin_layer.clone();
                m.avatar.clothes_layer = avatar.clothes_layer.clone();
                m.avatar.accessories_layer = avatar.accessories_layer.clone();
                m.avatar.emotion_layer = avatar.emotion_layer.clone();
            }
            on_update.run(members_vec.clone());
        }
    }
}

/// 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>>,
    _reconnect_trigger: Signal<u32>,
    _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>,
    _on_member_fading: Callback<FadingMember>,
    _on_welcome: Option<Callback<ChannelMemberInfo>>,
    _on_error: Option<Callback<WsError>>,
) -> (Signal<WsState>, WsSenderStorage) {
    let (ws_state, _) = signal(WsState::Disconnected);
    let sender: WsSenderStorage = StoredValue::new_local(None);
    (Signal::derive(move || ws_state.get()), sender)
}
