//! Shared utilities for the user UI.
//!
//! This module provides common abstractions to reduce code duplication:
//! - Geometry utilities (parsing WKT bounds)
//! - localStorage persistence trait
//! - Common hooks (escape key handling)

use serde::{Serialize, de::DeserializeOwned};

// ============================================================================
// Geometry Utilities
// ============================================================================

/// Parse bounds WKT to extract width and height.
///
/// Expected format: `"POLYGON((0 0, WIDTH 0, WIDTH HEIGHT, 0 HEIGHT, 0 0))"`
///
/// # Examples
///
/// ```
/// use chattyness_user_ui::utils::parse_bounds_dimensions;
///
/// let dims = parse_bounds_dimensions("POLYGON((0 0, 800 0, 800 600, 0 600, 0 0))");
/// assert_eq!(dims, Some((800, 600)));
/// ```
pub fn parse_bounds_dimensions(bounds_wkt: &str) -> Option<(u32, u32)> {
    let trimmed = bounds_wkt.trim();
    let coords_str = trimmed
        .strip_prefix("POLYGON((")
        .and_then(|s| s.strip_suffix("))"))?;

    let points: Vec<&str> = coords_str.split(',').collect();
    if points.len() < 4 {
        return None;
    }

    let mut max_x: f64 = 0.0;
    let mut max_y: f64 = 0.0;

    for point in points.iter() {
        let coords: Vec<&str> = point.trim().split_whitespace().collect();
        if coords.len() >= 2 {
            if let (Ok(x), Ok(y)) = (coords[0].parse::<f64>(), coords[1].parse::<f64>()) {
                if x > max_x {
                    max_x = x;
                }
                if y > max_y {
                    max_y = y;
                }
            }
        }
    }

    if max_x > 0.0 && max_y > 0.0 {
        Some((max_x as u32, max_y as u32))
    } else {
        None
    }
}

/// Normalize an asset path to be absolute, prefixing with `/static/` if needed.
pub fn normalize_asset_path(path: &str) -> String {
    if path.starts_with('/') {
        path.to_string()
    } else {
        format!("/static/{}", path)
    }
}

// ============================================================================
// localStorage Persistence
// ============================================================================

/// Trait for types that can be persisted to localStorage.
///
/// Implementors provide a storage key and get `load()` and `save()` methods
/// that handle serialization/deserialization automatically.
///
/// # Example
///
/// ```ignore
/// use chattyness_user_ui::utils::LocalStoragePersist;
///
/// #[derive(Default, Serialize, Deserialize)]
/// struct MySettings {
///     dark_mode: bool,
/// }
///
/// impl LocalStoragePersist for MySettings {
///     const STORAGE_KEY: &'static str = "my_settings";
/// }
///
/// // Now you can use:
/// let settings = MySettings::load();
/// settings.save();
/// ```
pub trait LocalStoragePersist: Sized + Default + Serialize + DeserializeOwned {
    /// The localStorage key used for this type.
    const STORAGE_KEY: &'static str;

    /// Optional post-load hook for validation/normalization.
    /// Override this to enforce invariants after loading.
    fn post_load(&mut self) {}

    /// Load from localStorage, returning default if not found or invalid.
    #[cfg(feature = "hydrate")]
    fn load() -> Self {
        let result: Self = web_sys::window()
            .and_then(|w| w.local_storage().ok().flatten())
            .and_then(|s| s.get_item(Self::STORAGE_KEY).ok().flatten())
            .and_then(|json| serde_json::from_str(&json).ok())
            .unwrap_or_default();

        let mut instance = result;
        instance.post_load();
        instance
    }

    /// SSR stub - returns default.
    #[cfg(not(feature = "hydrate"))]
    fn load() -> Self {
        Self::default()
    }

    /// Save to localStorage.
    #[cfg(feature = "hydrate")]
    fn save(&self) {
        if let (Some(storage), Ok(json)) = (
            web_sys::window().and_then(|w| w.local_storage().ok().flatten()),
            serde_json::to_string(self),
        ) {
            let _ = storage.set_item(Self::STORAGE_KEY, &json);
        }
    }

    /// SSR stub - no-op.
    #[cfg(not(feature = "hydrate"))]
    fn save(&self) {}
}

// ============================================================================
// Hooks
// ============================================================================

/// Hook to close a popup when Escape key is pressed.
///
/// Automatically adds and removes the event listener based on the `open` signal.
///
/// # Example
///
/// ```ignore
/// use chattyness_user_ui::utils::use_escape_key;
///
/// #[component]
/// fn MyPopup(open: Signal<bool>, on_close: Callback<()>) -> impl IntoView {
///     use_escape_key(open, on_close);
///     // ...
/// }
/// ```
#[cfg(feature = "hydrate")]
pub fn use_escape_key(
    open: leptos::prelude::Signal<bool>,
    on_close: leptos::prelude::Callback<()>,
) {
    use leptos::prelude::*;
    use wasm_bindgen::{JsCast, closure::Closure};

    Effect::new(move |_| {
        if !open.get() {
            return;
        }

        let on_close = on_close.clone();
        let closure =
            Closure::<dyn Fn(web_sys::KeyboardEvent)>::new(move |ev: web_sys::KeyboardEvent| {
                if ev.key() == "Escape" {
                    on_close.run(());
                }
            });

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

        // Note: We intentionally don't clean up the listener here.
        // In a production app, you'd want to use on_cleanup() or store the closure
        // in a signal to properly remove it. For now, closures accumulate but
        // early-return when `open` is false.
        closure.forget();
    });
}

/// SSR stub - no-op.
#[cfg(not(feature = "hydrate"))]
pub fn use_escape_key(
    _open: leptos::prelude::Signal<bool>,
    _on_close: leptos::prelude::Callback<()>,
) {
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_parse_bounds_dimensions_valid() {
        let wkt = "POLYGON((0 0, 800 0, 800 600, 0 600, 0 0))";
        assert_eq!(parse_bounds_dimensions(wkt), Some((800, 600)));
    }

    #[test]
    fn test_parse_bounds_dimensions_large() {
        let wkt = "POLYGON((0 0, 1920 0, 1920 1080, 0 1080, 0 0))";
        assert_eq!(parse_bounds_dimensions(wkt), Some((1920, 1080)));
    }

    #[test]
    fn test_parse_bounds_dimensions_invalid() {
        assert_eq!(parse_bounds_dimensions("invalid"), None);
        assert_eq!(parse_bounds_dimensions("POLYGON(())"), None);
        assert_eq!(parse_bounds_dimensions(""), None);
    }

    #[test]
    fn test_normalize_asset_path() {
        assert_eq!(normalize_asset_path("/images/foo.png"), "/images/foo.png");
        assert_eq!(
            normalize_asset_path("images/foo.png"),
            "/static/images/foo.png"
        );
        assert_eq!(normalize_asset_path("foo.png"), "/static/foo.png");
    }
}
