//! Realm landing page after login.

use std::collections::HashMap;

use leptos::prelude::*;
use leptos::reactive::owner::LocalStorage;
#[cfg(feature = "hydrate")]
use leptos::task::spawn_local;
#[cfg(feature = "hydrate")]
use leptos_router::hooks::use_navigate;
use leptos_router::hooks::use_params_map;
use uuid::Uuid;

use crate::components::{
    ActiveBubble, AvatarEditorPopup, Card, ChatInput, ConversationModal, EmotionKeybindings,
    FadingMember, InventoryPopup, KeybindingsPopup, MessageLog, NotificationHistoryModal,
    NotificationMessage, NotificationToast, RealmHeader, RealmSceneViewer, ReconnectionOverlay,
    SettingsPopup, ViewerSettings,
};
#[cfg(feature = "hydrate")]
use crate::components::{
    ChannelMemberInfo, ChatMessage, DEFAULT_BUBBLE_TIMEOUT_MS, FADE_DURATION_MS, HistoryEntry,
    WsError, add_to_history, use_channel_websocket,
};
use crate::utils::LocalStoragePersist;
#[cfg(feature = "hydrate")]
use crate::utils::parse_bounds_dimensions;
use chattyness_db::models::{
    AvatarWithPaths, ChannelMemberWithAvatar, EmotionAvailability, LooseProp, RealmRole,
    RealmWithUserRole, Scene,
};
#[cfg(feature = "hydrate")]
use chattyness_db::ws_messages::ClientMessage;

#[cfg(not(feature = "hydrate"))]
use crate::components::ws_client::WsSender;

/// Realm landing page component.
#[component]
pub fn RealmPage() -> impl IntoView {
    let params = use_params_map();
    #[cfg(feature = "hydrate")]
    let navigate = use_navigate();

    let slug = Signal::derive(move || params.read().get("slug").unwrap_or_default());

    // Channel member state
    let (members, set_members) = signal(Vec::<ChannelMemberWithAvatar>::new());
    let (channel_id, set_channel_id) = signal(Option::<uuid::Uuid>::None);

    // Chat focus coordination
    let (chat_focused, set_chat_focused) = signal(false);
    let (focus_chat_trigger, set_focus_chat_trigger) = signal(false);

    // Emotion availability for emote picker
    let (emotion_availability, set_emotion_availability) =
        signal(Option::<EmotionAvailability>::None);
    // Skin preview path for emote picker (position 4 of skin layer)
    let (skin_preview_path, set_skin_preview_path) = signal(Option::<String>::None);

    // Chat message state - use StoredValue for WASM compatibility (single-threaded)
    let message_log: StoredValue<MessageLog, LocalStorage> =
        StoredValue::new_local(MessageLog::new());
    let (active_bubbles, set_active_bubbles) =
        signal(HashMap::<(Option<Uuid>, Option<Uuid>), ActiveBubble>::new());

    // Inventory popup state
    let (inventory_open, set_inventory_open) = signal(false);

    // Settings popup state
    let (settings_open, set_settings_open) = signal(false);
    let viewer_settings = RwSignal::new(ViewerSettings::load());

    // Keybindings popup state
    let keybindings = RwSignal::new(EmotionKeybindings::load());
    let (keybindings_open, set_keybindings_open) = signal(false);

    // Avatar editor popup state
    let (avatar_editor_open, set_avatar_editor_open) = signal(false);
    // Store full avatar data for the editor
    let (full_avatar, set_full_avatar) = signal(Option::<AvatarWithPaths>::None);

    // Scene dimensions (extracted from bounds_wkt when scene loads)
    let (scene_dimensions, set_scene_dimensions) = signal((800.0_f64, 600.0_f64));

    // Chat focus prefix (: or /)
    let (focus_prefix, set_focus_prefix) = signal(':');

    // Loose props state
    let (loose_props, set_loose_props) = signal(Vec::<LooseProp>::new());

    // Fading members state (members that are fading out after timeout disconnect)
    let (fading_members, set_fading_members) = signal(Vec::<FadingMember>::new());

    // Track user's current position for saving on beforeunload
    let (current_position, set_current_position) = signal((400.0_f64, 300.0_f64));

    // Current user identity (received from WebSocket Welcome message)
    let (current_user_id, set_current_user_id) = signal(Option::<Uuid>::None);
    let (current_guest_session_id, set_current_guest_session_id) = signal(Option::<Uuid>::None);
    // Whether the current user is a guest (has the 'guest' tag)
    let (is_guest, set_is_guest) = signal(false);

    // Whisper target - when set, triggers pre-fill in ChatInput
    let (whisper_target, set_whisper_target) = signal(Option::<String>::None);

    // Notification state for cross-scene whispers
    let (current_notification, set_current_notification) =
        signal(Option::<NotificationMessage>::None);
    let (history_modal_open, set_history_modal_open) = signal(false);
    let (conversation_modal_open, set_conversation_modal_open) = signal(false);
    let (conversation_partner, set_conversation_partner) = signal(String::new());
    // Track all whisper messages for conversation view (client-side only)
    #[cfg(feature = "hydrate")]
    let whisper_messages: StoredValue<Vec<ChatMessage>, LocalStorage> =
        StoredValue::new_local(Vec::new());
    // Current user's display name (for conversation modal)
    let (current_display_name, set_current_display_name) = signal(String::new());

    // Error notification state (for whisper failures, etc.)
    let (error_message, set_error_message) = signal(Option::<String>::None);

    // Reconnection trigger - increment to force WebSocket reconnection
    let (reconnect_trigger, set_reconnect_trigger) = signal(0u32);

    let realm_data = LocalResource::new(move || {
        let slug = slug.get();
        async move {
            if slug.is_empty() {
                return None;
            }

            #[cfg(feature = "hydrate")]
            {
                use gloo_net::http::Request;
                let response = Request::get(&format!("/api/realms/{}", slug)).send().await;
                match response {
                    Ok(resp) if resp.ok() => resp.json::<RealmWithUserRole>().await.ok(),
                    _ => None,
                }
            }
            #[cfg(not(feature = "hydrate"))]
            {
                let _ = slug;
                None::<RealmWithUserRole>
            }
        }
    });

    // Fetch entry scene for the realm
    let entry_scene = LocalResource::new(move || {
        let slug = slug.get();
        async move {
            if slug.is_empty() {
                return None;
            }

            #[cfg(feature = "hydrate")]
            {
                use gloo_net::http::Request;
                let response = Request::get(&format!("/api/realms/{}/entry-scene", slug))
                    .send()
                    .await;
                match response {
                    Ok(resp) if resp.ok() => resp.json::<Scene>().await.ok(),
                    _ => None,
                }
            }
            #[cfg(not(feature = "hydrate"))]
            {
                let _ = slug;
                None::<Scene>
            }
        }
    });

    // Fetch full avatar with paths for client-side emotion computation
    #[cfg(feature = "hydrate")]
    {
        let slug_for_avatar = slug.clone();
        Effect::new(move |_| {
            use gloo_net::http::Request;

            let current_slug = slug_for_avatar.get();
            if current_slug.is_empty() {
                return;
            }

            // Fetch full avatar with all paths resolved
            spawn_local(async move {
                let response = Request::get(&format!("/api/realms/{}/avatar", current_slug))
                    .send()
                    .await;
                if let Ok(resp) = response {
                    if resp.ok() {
                        if let Ok(avatar) = resp.json::<AvatarWithPaths>().await {
                            // Compute emotion availability client-side
                            let avail = avatar.compute_emotion_availability();
                            set_emotion_availability.set(Some(avail));
                            // Get skin layer position 4 (center) for preview
                            set_skin_preview_path.set(avatar.skin_layer[4].clone());
                            // Store full avatar for the editor
                            set_full_avatar.set(Some(avatar));
                        }
                    }
                }
            });
        });
    }

    // WebSocket connection for real-time updates
    #[cfg(feature = "hydrate")]
    let on_members_update = Callback::new(move |new_members: Vec<ChannelMemberWithAvatar>| {
        // When members are updated (including rejoins), remove any matching fading members
        set_fading_members.update(|fading| {
            fading.retain(|f| {
                !new_members.iter().any(|m| {
                    m.member.user_id == f.member.member.user_id
                        && m.member.guest_session_id == f.member.member.guest_session_id
                })
            });
        });
        set_members.set(new_members);
    });

    // Chat message callback
    #[cfg(feature = "hydrate")]
    let on_chat_message = Callback::new(move |msg: ChatMessage| {
        // Add to message log
        message_log.update_value(|log| log.push(msg.clone()));

        // Handle whispers
        if msg.is_whisper {
            // Track whisper for conversation view
            whisper_messages.update_value(|msgs| {
                msgs.push(msg.clone());
                // Keep last 100 whisper messages
                if msgs.len() > 100 {
                    msgs.remove(0);
                }
            });

            // Add to notification history for persistence
            add_to_history(HistoryEntry::from_whisper(&msg));

            if msg.is_same_scene {
                // Same scene whisper: show as italic bubble (handled by bubble rendering)
                let key = (msg.user_id, msg.guest_session_id);
                let expires_at = msg.timestamp + DEFAULT_BUBBLE_TIMEOUT_MS;
                set_active_bubbles.update(|bubbles| {
                    bubbles.insert(
                        key,
                        ActiveBubble {
                            message: msg,
                            expires_at,
                        },
                    );
                });
            } else {
                // Cross-scene whisper: show as notification toast
                set_current_notification.set(Some(NotificationMessage::from_chat_message(msg)));
            }
        } else {
            // Regular broadcast: show as bubble
            let key = (msg.user_id, msg.guest_session_id);
            let expires_at = msg.timestamp + DEFAULT_BUBBLE_TIMEOUT_MS;
            set_active_bubbles.update(|bubbles| {
                bubbles.insert(
                    key,
                    ActiveBubble {
                        message: msg,
                        expires_at,
                    },
                );
            });
        }
    });

    // Loose props callbacks
    #[cfg(feature = "hydrate")]
    let on_loose_props_sync = Callback::new(move |props: Vec<LooseProp>| {
        set_loose_props.set(props);
    });

    #[cfg(feature = "hydrate")]
    let on_prop_dropped = Callback::new(move |prop: LooseProp| {
        set_loose_props.update(|props| {
            props.push(prop);
        });
    });

    #[cfg(feature = "hydrate")]
    let on_prop_picked_up = Callback::new(move |prop_id: Uuid| {
        set_loose_props.update(|props| {
            props.retain(|p| p.id != prop_id);
        });
    });

    // Callback when a member starts fading (timeout disconnect)
    #[cfg(feature = "hydrate")]
    let on_member_fading = Callback::new(move |fading: FadingMember| {
        set_fading_members.update(|members| {
            // Remove any existing entry for this user (shouldn't happen, but be safe)
            members.retain(|m| {
                m.member.member.user_id != fading.member.member.user_id
                    || m.member.member.guest_session_id != fading.member.member.guest_session_id
            });
            members.push(fading);
        });
    });

    // Callback to capture current user identity from Welcome message
    #[cfg(feature = "hydrate")]
    let on_welcome = Callback::new(move |info: ChannelMemberInfo| {
        set_current_user_id.set(info.user_id);
        set_current_guest_session_id.set(info.guest_session_id);
        set_current_display_name.set(info.display_name.clone());
        set_is_guest.set(info.is_guest);
    });

    // Callback for WebSocket errors (whisper failures, etc.)
    #[cfg(feature = "hydrate")]
    let on_ws_error = Callback::new(move |error: WsError| {
        // Display user-friendly error message
        let msg = match error.code.as_str() {
            "WHISPER_TARGET_NOT_FOUND" => error.message,
            _ => format!("Error: {}", error.message),
        };
        set_error_message.set(Some(msg));
        // Auto-dismiss after 5 seconds
        use gloo_timers::callback::Timeout;
        Timeout::new(5000, move || {
            set_error_message.set(None);
        })
        .forget();
    });

    #[cfg(feature = "hydrate")]
    let (ws_state, ws_sender) = use_channel_websocket(
        slug,
        Signal::derive(move || channel_id.get()),
        Signal::derive(move || reconnect_trigger.get()),
        on_members_update,
        on_chat_message,
        on_loose_props_sync,
        on_prop_dropped,
        on_prop_picked_up,
        on_member_fading,
        Some(on_welcome),
        Some(on_ws_error),
    );

    // Set channel ID and scene dimensions when scene loads
    // Note: Currently using scene.id as the channel_id since channel_members
    // uses scenes directly. Proper channel infrastructure can be added later.
    #[cfg(feature = "hydrate")]
    {
        Effect::new(move |_| {
            let Some(scene) = entry_scene.get().flatten() else {
                return;
            };
            set_channel_id.set(Some(scene.id));

            // Extract scene dimensions from bounds_wkt
            if let Some((w, h)) = parse_bounds_dimensions(&scene.bounds_wkt) {
                set_scene_dimensions.set((w as f64, h as f64));
            }
        });
    }

    // Cleanup expired speech bubbles and fading members every second
    #[cfg(feature = "hydrate")]
    {
        use gloo_timers::callback::Interval;

        let cleanup_interval = Interval::new(1000, move || {
            let now = js_sys::Date::now() as i64;
            // Clean up expired bubbles
            set_active_bubbles.update(|bubbles| {
                bubbles.retain(|_, bubble| bubble.expires_at > now);
            });
            // Clean up completed fading members
            set_fading_members.update(|members| {
                members.retain(|m| now - m.fade_start < FADE_DURATION_MS);
            });
        });
        // Keep interval alive
        std::mem::forget(cleanup_interval);
    }

    // Handle position update via WebSocket
    #[cfg(feature = "hydrate")]
    let on_move = Callback::new(move |(x, y): (f64, f64)| {
        // Track position for saving on beforeunload
        set_current_position.set((x, y));
        ws_sender.with_value(|sender| {
            if let Some(send_fn) = sender {
                send_fn(ClientMessage::UpdatePosition { x, y });
            }
        });
    });

    #[cfg(not(feature = "hydrate"))]
    let on_move = Callback::new(move |(_x, _y): (f64, f64)| {});

    // Handle prop click (pickup) via WebSocket
    #[cfg(feature = "hydrate")]
    let on_prop_click = Callback::new(move |prop_id: Uuid| {
        ws_sender.with_value(|sender| {
            if let Some(send_fn) = sender {
                send_fn(ClientMessage::PickUpProp {
                    loose_prop_id: prop_id,
                });
            }
        });
    });

    #[cfg(not(feature = "hydrate"))]
    let on_prop_click = Callback::new(move |_prop_id: Uuid| {});

    // Handle global keyboard shortcuts (e+0-9 for emotions, : for chat focus)
    #[cfg(feature = "hydrate")]
    {
        use std::cell::RefCell;
        use std::rc::Rc;
        use wasm_bindgen::{JsCast, closure::Closure};

        let closure_holder: Rc<RefCell<Option<Closure<dyn Fn(web_sys::KeyboardEvent)>>>> =
            Rc::new(RefCell::new(None));
        let closure_holder_clone = closure_holder.clone();

        Effect::new(move |_| {
            // Cleanup previous closure if any
            if let Some(old_closure) = closure_holder_clone.borrow_mut().take() {
                if let Some(window) = web_sys::window() {
                    let _ = window.remove_event_listener_with_callback(
                        "keydown",
                        old_closure.as_ref().unchecked_ref(),
                    );
                }
            }

            let current_slug = slug.get();
            if current_slug.is_empty() {
                return;
            }

            // Track if 'e' was pressed (for e+0-9 emotion sequence)
            let e_pressed: Rc<RefCell<bool>> = Rc::new(RefCell::new(false));
            let e_pressed_clone = e_pressed.clone();

            let closure = Closure::new(move |ev: web_sys::KeyboardEvent| {
                let key = ev.key();

                // If chat is focused, let it handle all keys
                // Use get_untracked() since we're in a JS event handler, not a reactive context
                if chat_focused.get_untracked() {
                    *e_pressed_clone.borrow_mut() = false;
                    return;
                }

                // Handle space to focus chat input (no prefix)
                if key == " " {
                    set_focus_prefix.set(' ');
                    set_focus_chat_trigger.set(true);
                    use gloo_timers::callback::Timeout;
                    Timeout::new(100, move || {
                        set_focus_chat_trigger.set(false);
                    })
                    .forget();
                    ev.prevent_default();
                    return;
                }

                // Handle ':' to focus chat input with colon prefix
                if key == ":" {
                    set_focus_prefix.set(':');
                    set_focus_chat_trigger.set(true);
                    // Reset trigger after a short delay so it can be triggered again
                    use gloo_timers::callback::Timeout;
                    Timeout::new(100, move || {
                        set_focus_chat_trigger.set(false);
                    })
                    .forget();
                    ev.prevent_default();
                    return;
                }

                // Handle '/' to focus chat input with slash prefix
                if key == "/" {
                    set_focus_prefix.set('/');
                    set_focus_chat_trigger.set(true);
                    use gloo_timers::callback::Timeout;
                    Timeout::new(100, move || {
                        set_focus_chat_trigger.set(false);
                    })
                    .forget();
                    ev.prevent_default();
                    return;
                }

                // Handle 's' to toggle settings
                if key == "s" || key == "S" {
                    set_settings_open.update(|v| *v = !*v);
                    ev.prevent_default();
                    return;
                }

                // Handle arrow keys for panning (only in pan mode)
                let settings = viewer_settings.get_untracked();
                if settings.panning_enabled {
                    let pan_step = 50.0;
                    let scroll_delta = match key.as_str() {
                        "ArrowLeft" => Some((-pan_step, 0.0)),
                        "ArrowRight" => Some((pan_step, 0.0)),
                        "ArrowUp" => Some((0.0, -pan_step)),
                        "ArrowDown" => Some((0.0, pan_step)),
                        _ => None,
                    };

                    if let Some((dx, dy)) = scroll_delta {
                        // Find the scene container and scroll it
                        if let Some(document) = web_sys::window().and_then(|w| w.document()) {
                            if let Some(container) =
                                document.query_selector(".scene-container").ok().flatten()
                            {
                                let container_el: web_sys::Element = container;
                                container_el.scroll_by_with_x_and_y(dx, dy);
                            }
                        }
                        ev.prevent_default();
                        return;
                    }

                    // Handle +/- for zoom
                    let zoom_delta = match key.as_str() {
                        "+" | "=" => Some(0.25),
                        "-" | "_" => Some(-0.25),
                        _ => None,
                    };

                    if let Some(delta) = zoom_delta {
                        viewer_settings.update(|s| s.adjust_zoom(delta));
                        viewer_settings.get_untracked().save();
                        ev.prevent_default();
                        return;
                    }
                }

                // Handle 'i' to toggle inventory
                if key == "i" || key == "I" {
                    set_inventory_open.update(|v| *v = !*v);
                    ev.prevent_default();
                    return;
                }

                // Handle 'k' to toggle keybindings
                if key == "k" || key == "K" {
                    set_keybindings_open.update(|v| *v = !*v);
                    ev.prevent_default();
                    return;
                }

                // Handle 'a' to toggle avatar editor
                if key == "a" || key == "A" {
                    set_avatar_editor_open.update(|v| *v = !*v);
                    ev.prevent_default();
                    return;
                }

                // Check if 'e' key was pressed
                if key == "e" || key == "E" {
                    *e_pressed_clone.borrow_mut() = true;
                    return;
                }

                // Check for 0-9, q, w after 'e' was pressed (emotion keybindings)
                if *e_pressed_clone.borrow() {
                    *e_pressed_clone.borrow_mut() = false; // Reset regardless of outcome
                    let bindings = keybindings.get_untracked();
                    if let Some(emotion_state) = bindings.get_emotion_for_key(&key) {
                        let emotion = emotion_state.to_string();
                        #[cfg(debug_assertions)]
                        web_sys::console::log_1(
                            &format!("[Emotion] Sending emotion {}", emotion).into(),
                        );
                        ws_sender.with_value(|sender| {
                            if let Some(send_fn) = sender {
                                send_fn(ClientMessage::UpdateEmotion { emotion });
                            }
                        });
                        ev.prevent_default();
                    }
                } else {
                    // Any other key resets the 'e' state
                    *e_pressed_clone.borrow_mut() = false;
                }
            });

            if let Some(window) = web_sys::window() {
                let _ = window
                    .add_event_listener_with_callback("keydown", closure.as_ref().unchecked_ref());
            }

            // Store the closure for cleanup
            *closure_holder_clone.borrow_mut() = Some(closure);
        });

        // Save position on page unload (beforeunload event)
        Effect::new({
            let ws_sender = ws_sender.clone();
            move |_| {
                let Some(window) = web_sys::window() else {
                    return;
                };

                let handler = Closure::<dyn Fn(web_sys::BeforeUnloadEvent)>::new({
                    let ws_sender = ws_sender.clone();
                    move |_: web_sys::BeforeUnloadEvent| {
                        let (x, y) = current_position.get_untracked();
                        ws_sender.with_value(|sender| {
                            if let Some(send_fn) = sender {
                                send_fn(ClientMessage::UpdatePosition { x, y });
                            }
                        });
                    }
                });

                let _ = window.add_event_listener_with_callback(
                    "beforeunload",
                    handler.as_ref().unchecked_ref(),
                );
                handler.forget();
            }
        });
    }

    // Callback for chat focus changes
    let on_chat_focus_change = Callback::new(move |focused: bool| {
        set_chat_focused.set(focused);
    });

    // Create logout callback (WebSocket disconnects automatically)
    let on_logout = Callback::new(move |_: ()| {
        #[cfg(feature = "hydrate")]
        {
            use gloo_net::http::Request;
            let navigate = navigate.clone();

            spawn_local(async move {
                // WebSocket close handles channel leave automatically
                let _: Result<gloo_net::http::Response, gloo_net::Error> =
                    Request::post("/api/auth/logout").send().await;
                navigate("/", Default::default());
            });
        }
    });

    view! {
        <div class="h-screen bg-gray-900 text-white flex flex-col overflow-hidden">
            <Suspense fallback=move || {
                view! {
                    <div class="flex items-center justify-center min-h-screen">
                        <p class="text-gray-400">"Loading realm..."</p>
                    </div>
                }
            }>
                {move || {
                    let on_logout = on_logout.clone();
                    let on_move = on_move.clone();
                    realm_data
                        .get()
                        .map(|maybe_data| {
                            match maybe_data {
                                Some(data) => {
                                    let realm = data.realm;
                                    let user_role = data.user_role;

                                    // Determine if user can access admin
                                    // Admin visible for: Owner, Moderator, or staff
                                    let can_admin = matches!(
                                        user_role,
                                        Some(RealmRole::Owner) | Some(RealmRole::Moderator)
                                    );

                                    // Get scene name and description for header
                                    let scene_info = entry_scene
                                        .get()
                                        .flatten()
                                        .map(|s| (s.name.clone(), s.description.clone()))
                                        .unwrap_or_else(|| ("Loading...".to_string(), None));

                                    let realm_name = realm.name.clone();
                                    let realm_slug_val = realm.slug.clone();
                                    let realm_description = realm.tagline.clone();
                                    // Derive online count reactively from members signal
                                    let online_count = Signal::derive(move || members.get().len() as i32);
                                    let total_members = realm.member_count;
                                    let max_capacity = realm.max_users;
                                    let scene_name = scene_info.0;
                                    let scene_description = scene_info.1;

                                    view! {
                                        <RealmHeader
                                            realm_name=realm_name
                                            realm_slug=realm_slug_val.clone()
                                            realm_description=realm_description
                                            scene_name=scene_name
                                            scene_description=scene_description
                                            online_count=online_count
                                            total_members=total_members
                                            max_capacity=max_capacity
                                            can_admin=can_admin
                                            on_logout=on_logout.clone()
                                        />
                                        <main class="flex-1 w-full">
                                            // Scene viewer - full width
                                            <Suspense fallback=move || {
                                                view! {
                                                    <div class="flex items-center justify-center py-12">
                                                        <p class="text-gray-400">"Loading scene..."</p>
                                                    </div>
                                                }
                                            }>
                                                {move || {
                                                    let on_move = on_move.clone();
                                                    let on_prop_click = on_prop_click.clone();
                                                    let on_chat_focus_change = on_chat_focus_change.clone();
                                                    let realm_slug_for_viewer = realm_slug_val.clone();
                                                    #[cfg(feature = "hydrate")]
                                                    let ws_sender_clone = ws_sender.clone();
                                                    entry_scene
                                                        .get()
                                                        .map(|maybe_scene| {
                                                            match maybe_scene {
                                                                Some(scene) => {
                                                                    let members_signal = Signal::derive(move || members.get());
                                                                    let emotion_avail_signal = Signal::derive(move || emotion_availability.get());
                                                                    let skin_path_signal = Signal::derive(move || skin_preview_path.get());
                                                                    let focus_trigger_signal = Signal::derive(move || focus_chat_trigger.get());
                                                                    #[cfg(feature = "hydrate")]
                                                                    let ws_for_chat = ws_sender_clone.clone();
                                                                    #[cfg(not(feature = "hydrate"))]
                                                                    let ws_for_chat: StoredValue<Option<WsSender>, LocalStorage> = StoredValue::new_local(None);
                                                                    let active_bubbles_signal = Signal::derive(move || active_bubbles.get());
                                                                    let loose_props_signal = Signal::derive(move || loose_props.get());
                                                                    let focus_prefix_signal = Signal::derive(move || focus_prefix.get());
                                                                    let on_open_settings_cb = Callback::new(move |_: ()| {
                                                                        set_settings_open.set(true);
                                                                    });
                                                                    let on_open_inventory_cb = Callback::new(move |_: ()| {
                                                                        set_inventory_open.set(true);
                                                                    });
                                                                    let whisper_target_signal = Signal::derive(move || whisper_target.get());
                                                                    let on_whisper_request_cb = Callback::new(move |target: String| {
                                                                        set_whisper_target.set(Some(target));
                                                                    });
                                                                    view! {
                                                                        <div class="relative w-full">
                                                                            <RealmSceneViewer
                                                                                scene=scene
                                                                                realm_slug=realm_slug_for_viewer.clone()
                                                                                members=members_signal
                                                                                active_bubbles=active_bubbles_signal
                                                                                loose_props=loose_props_signal
                                                                                on_move=on_move.clone()
                                                                                on_prop_click=on_prop_click.clone()
                                                                                settings=Signal::derive(move || viewer_settings.get())
                                                                                on_zoom_change=Callback::new(move |delta: f64| {
                                                                                    viewer_settings.update(|s| {
                                                                                        s.adjust_zoom(delta);
                                                                                        s.save();
                                                                                    });
                                                                                })
                                                                                fading_members=Signal::derive(move || fading_members.get())
                                                                                current_user_id=Signal::derive(move || current_user_id.get())
                                                                                current_guest_session_id=Signal::derive(move || current_guest_session_id.get())
                                                                                is_guest=Signal::derive(move || is_guest.get())
                                                                                on_whisper_request=on_whisper_request_cb
                                                                            />
                                                                            <div class="absolute bottom-0 left-0 right-0 z-10 pb-4 px-4 pointer-events-none">
                                                                                <ChatInput
                                                                                    ws_sender=ws_for_chat
                                                                                    emotion_availability=emotion_avail_signal
                                                                                    skin_preview_path=skin_path_signal
                                                                                    focus_trigger=focus_trigger_signal
                                                                                    focus_prefix=focus_prefix_signal
                                                                                    on_focus_change=on_chat_focus_change.clone()
                                                                                    on_open_settings=on_open_settings_cb
                                                                                    on_open_inventory=on_open_inventory_cb
                                                                                    whisper_target=whisper_target_signal
                                                                                />
                                                                            </div>
                                                                        </div>
                                                                    }
                                                                        .into_any()
                                                                }
                                                                None => {
                                                                    view! {
                                                                        <div class="max-w-4xl mx-auto px-4 py-8">
                                                                            <Card class="p-8 text-center">
                                                                                <p class="text-gray-400">
                                                                                    "No scenes have been created for this realm yet."
                                                                                </p>
                                                                            </Card>
                                                                        </div>
                                                                    }
                                                                        .into_any()
                                                                }
                                                            }
                                                        })
                                                }}
                                            </Suspense>
                                        </main>

                                        // Inventory popup
                                        {
                                            #[cfg(feature = "hydrate")]
                                            let ws_sender_for_inv = ws_sender.clone();
                                            #[cfg(not(feature = "hydrate"))]
                                            let ws_sender_for_inv: StoredValue<Option<WsSender>, LocalStorage> = StoredValue::new_local(None);
                                            view! {
                                                <InventoryPopup
                                                    open=Signal::derive(move || inventory_open.get())
                                                    on_close=Callback::new(move |_: ()| {
                                                        set_inventory_open.set(false);
                                                    })
                                                    ws_sender=ws_sender_for_inv
                                                    realm_slug=Signal::derive(move || slug.get())
                                                    is_guest=Signal::derive(move || is_guest.get())
                                                />
                                            }
                                        }

                                        // Settings popup
                                        <SettingsPopup
                                            open=Signal::derive(move || settings_open.get())
                                            settings=viewer_settings
                                            on_close=Callback::new(move |_: ()| {
                                                set_settings_open.set(false);
                                            })
                                            scene_dimensions=scene_dimensions.get()
                                        />

                                        // Keybindings popup
                                        <KeybindingsPopup
                                            open=Signal::derive(move || keybindings_open.get())
                                            keybindings=keybindings
                                            emotion_availability=Signal::derive(move || emotion_availability.get())
                                            skin_preview_path=Signal::derive(move || skin_preview_path.get())
                                            on_close=Callback::new(move |_: ()| set_keybindings_open.set(false))
                                        />

                                        // Avatar editor popup
                                        {
                                            #[cfg(feature = "hydrate")]
                                            let ws_sender_for_avatar = ws_sender.clone();
                                            #[cfg(not(feature = "hydrate"))]
                                            let ws_sender_for_avatar: StoredValue<Option<WsSender>, LocalStorage> = StoredValue::new_local(None);
                                            view! {
                                                <AvatarEditorPopup
                                                    open=Signal::derive(move || avatar_editor_open.get())
                                                    on_close=Callback::new(move |_: ()| set_avatar_editor_open.set(false))
                                                    avatar=Signal::derive(move || full_avatar.get())
                                                    realm_slug=Signal::derive(move || slug.get())
                                                    on_avatar_update=Callback::new(move |updated: AvatarWithPaths| {
                                                        set_full_avatar.set(Some(updated.clone()));
                                                        // Update emotion availability for the emotion picker
                                                        let avail = updated.compute_emotion_availability();
                                                        set_emotion_availability.set(Some(avail));
                                                        // Update skin preview
                                                        set_skin_preview_path.set(updated.skin_layer[4].clone());
                                                    })
                                                    ws_sender=ws_sender_for_avatar
                                                    is_guest=Signal::derive(move || is_guest.get())
                                                />
                                            }
                                        }

                                        // Notification toast for cross-scene whispers
                                        <NotificationToast
                                            notification=Signal::derive(move || current_notification.get())
                                            on_reply=Callback::new(move |name: String| {
                                                set_whisper_target.set(Some(name));
                                            })
                                            on_context=Callback::new(move |name: String| {
                                                set_conversation_partner.set(name);
                                                set_conversation_modal_open.set(true);
                                            })
                                            on_history=Callback::new(move |_: ()| {
                                                set_history_modal_open.set(true);
                                            })
                                            on_dismiss=Callback::new(move |_: Uuid| {
                                                set_current_notification.set(None);
                                            })
                                        />

                                        // Error toast (whisper failures, etc.)
                                        <Show when=move || error_message.get().is_some()>
                                            {move || {
                                                if let Some(msg) = error_message.get() {
                                                    view! {
                                                        <div class="fixed top-4 left-1/2 -translate-x-1/2 z-50 animate-slide-in-down">
                                                            <div class="bg-red-900/90 border border-red-500/50 rounded-lg shadow-lg px-6 py-3 flex items-center gap-3">
                                                                <span class="text-red-300 text-lg">"⚠"</span>
                                                                <span class="text-gray-200">{msg}</span>
                                                                <button
                                                                    class="text-gray-400 hover:text-white ml-2"
                                                                    on:click=move |_| set_error_message.set(None)
                                                                >
                                                                    "×"
                                                                </button>
                                                            </div>
                                                        </div>
                                                    }.into_any()
                                                } else {
                                                    ().into_any()
                                                }
                                            }}
                                        </Show>

                                        // Notification history modal
                                        <NotificationHistoryModal
                                            open=Signal::derive(move || history_modal_open.get())
                                            on_close=Callback::new(move |_: ()| set_history_modal_open.set(false))
                                            on_reply=Callback::new(move |name: String| {
                                                set_whisper_target.set(Some(name));
                                            })
                                            on_context=Callback::new(move |name: String| {
                                                set_conversation_partner.set(name);
                                                set_conversation_modal_open.set(true);
                                            })
                                        />

                                        // Conversation modal
                                        {
                                            #[cfg(feature = "hydrate")]
                                            let ws_sender_for_convo = ws_sender.clone();
                                            #[cfg(not(feature = "hydrate"))]
                                            let ws_sender_for_convo: StoredValue<Option<WsSender>, LocalStorage> = StoredValue::new_local(None);
                                            #[cfg(feature = "hydrate")]
                                            let filtered_whispers = Signal::derive(move || {
                                                let partner = conversation_partner.get();
                                                whisper_messages.with_value(|msgs| {
                                                    msgs.iter()
                                                        .filter(|m| {
                                                            m.display_name == partner ||
                                                            (m.is_whisper && m.display_name == current_display_name.get())
                                                        })
                                                        .cloned()
                                                        .collect::<Vec<_>>()
                                                })
                                            });
                                            #[cfg(not(feature = "hydrate"))]
                                            let filtered_whispers: Signal<Vec<crate::components::ChatMessage>> = Signal::derive(|| Vec::new());
                                            view! {
                                                <ConversationModal
                                                    open=Signal::derive(move || conversation_modal_open.get())
                                                    on_close=Callback::new(move |_: ()| set_conversation_modal_open.set(false))
                                                    partner_name=Signal::derive(move || conversation_partner.get())
                                                    messages=filtered_whispers
                                                    current_user_name=Signal::derive(move || current_display_name.get())
                                                    ws_sender=ws_sender_for_convo
                                                />
                                            }
                                        }

                                        // Reconnection overlay - shown when WebSocket disconnects
                                        {
                                            #[cfg(feature = "hydrate")]
                                            let ws_state_for_overlay = ws_state;
                                            #[cfg(not(feature = "hydrate"))]
                                            let ws_state_for_overlay = Signal::derive(|| crate::components::ws_client::WsState::Disconnected);
                                            view! {
                                                <ReconnectionOverlay
                                                    ws_state=ws_state_for_overlay
                                                    on_reconnect=Callback::new(move |_: ()| {
                                                        set_reconnect_trigger.update(|t| *t += 1);
                                                    })
                                                />
                                            }
                                        }
                                    }
                                        .into_any()
                                }
                                None => {
                                    view! {
                                        <div class="flex items-center justify-center min-h-screen">
                                            <Card class="p-8 text-center max-w-md">
                                                <div class="mx-auto w-20 h-20 rounded-full bg-red-900/20 flex items-center justify-center mb-4">
                                                    <img
                                                        src="/icons/x.svg"
                                                        alt=""
                                                        class="w-10 h-10"
                                                        aria-hidden="true"
                                                    />
                                                </div>
                                                <h2 class="text-xl font-semibold text-white mb-2">
                                                    "Realm Not Found"
                                                </h2>
                                                <p class="text-gray-400 mb-6">
                                                    "The realm you're looking for doesn't exist or you don't have access."
                                                </p>
                                                <a href="/" class="btn-primary inline-block">
                                                    "Back to Home"
                                                </a>
                                            </Card>
                                        </div>
                                    }
                                        .into_any()
                                }
                            }
                        })
                }}
            </Suspense>
        </div>
    }
}
