/// Runtime RBAC policy — delivered entirely via WebSocket from the server.
///
/// `AUTH_PATTERNS` is always `&[]` at compile time; no RBAC state is baked into
/// the WASM binary.  On startup `init_policy_signal` creates an empty signal.
/// `connect_policy_ws` then opens `/api/ws`; the server sends the current
/// `anonymous_deny_paths` immediately on connect and re-sends on every
/// `rbac.ncl` hot-reload.  All reactive consumers (menu filter, SPA guard)
/// update in-place without a WASM rebuild.
///
/// The same WebSocket also carries JWT lifecycle events (`TokenRefreshRequested`,
/// `SessionRevoked`, `NewDeviceLogin`, `AccountUpdated`, `SecurityAlert`) so the
/// server can drive client-side auth state transitions without polling.
///
/// Security boundary: the server's HTTP middleware enforces RBAC on every request.
/// The WASM signal is a UX layer only — it hides menu items and prevents SPA
/// navigation to restricted paths, but the server will reject any unauthorised
/// request regardless of client state.
use leptos::prelude::*;

/// Initialise the deny-paths signal from compile-time `AUTH_PATTERNS`.
///
/// Call this once inside `AppComponent` before providing it as context.
/// Both SSR and WASM call this; only WASM will subsequently connect the WebSocket.
pub fn init_policy_signal() -> RwSignal<Vec<String>> {
    let initial: Vec<String> = crate::routes::AUTH_PATTERNS
        .iter()
        .map(|s| s.to_string())
        .collect();
    RwSignal::new(initial)
}

/// Connect to `/api/ws` and update reactive state whenever the server broadcasts
/// policy or JWT lifecycle messages.  Reconnects automatically on disconnect.
///
/// `auth_state` and `toast_sig` are resolved from context here — before the first
/// `.await` — so they remain usable inside the async loop without requiring
/// `use_context` across async boundaries.
///
/// No-op on any non-WASM target.
#[cfg(target_arch = "wasm32")]
pub fn connect_policy_ws(
    deny_paths: RwSignal<Vec<String>>,
    toast_sig: RwSignal<Vec<rustelo_components_leptos::toast::ToastNotification>>,
    set_path: WriteSignal<String>,
) {
    use futures::StreamExt;
    use gloo_net::websocket::{futures::WebSocket, Message};
    use rustelo_components_leptos::toast::{push_toast_to, ToastKind};

    // Capture auth state synchronously (before any .await) — the reactive owner
    // is still the component's owner at this point.
    let auth_state = use_context::<rustelo_core_lib::state::AuthState>();

    // Local mirror of the relevant server-side `ContentNotification` variants.
    // `#[serde(other)]` silently absorbs all unknown variants.
    #[derive(serde::Deserialize)]
    #[serde(tag = "type", content = "data")]
    enum PolicyMsg {
        RbacPolicyUpdated {
            anonymous_deny_paths: Vec<String>,
        },
        TokenRefreshRequested,
        SessionRevoked {
            reason: String,
        },
        NewDeviceLogin {
            session_id: String,
        },
        AccountUpdated {
            field: String,
        },
        SecurityAlert {
            message: String,
        },
        ToastBroadcast {
            kind: String,
            message: String,
        },
        #[serde(other)]
        Other,
    }

    leptos::task::spawn_local(async move {
        loop {
            let url = ws_url("/api/ws");

            let ws = match WebSocket::open(&url) {
                Ok(ws) => ws,
                Err(e) => {
                    web_sys::console::warn_1(
                        &format!("policy WS: open failed for {}: {:?}", url, e).into(),
                    );
                    reconnect_delay().await;
                    continue;
                }
            };

            let (_, mut read) = ws.split();

            while let Some(msg) = read.next().await {
                match msg {
                    Ok(Message::Text(text)) => {
                        match serde_json::from_str::<PolicyMsg>(&text) {
                            Ok(PolicyMsg::RbacPolicyUpdated { anonymous_deny_paths }) => {
                                deny_paths.set(anonymous_deny_paths);
                            }
                            Ok(PolicyMsg::TokenRefreshRequested) => {
                                leptos::task::spawn_local(async {
                                    let _ = rustelo_components_leptos::server_refresh_token().await;
                                });
                            }
                            Ok(PolicyMsg::SessionRevoked { .. }) => {
                                if let Some(ref auth) = auth_state {
                                    auth.clear_auth();
                                }
                                push_toast_to(
                                    toast_sig,
                                    ToastKind::Error,
                                    "Your session was revoked. Please log in again.",
                                );
                                let current = web_sys::window()
                                    .and_then(|w| w.location().pathname().ok())
                                    .unwrap_or_default();
                                rustelo_core_lib::utils::nav::anchor_navigate(
                                    set_path,
                                    &format!("/login?return={}", current),
                                );
                            }
                            Ok(PolicyMsg::NewDeviceLogin { .. }) => {
                                push_toast_to(
                                    toast_sig,
                                    ToastKind::Warning,
                                    "New login detected from another device.",
                                );
                            }
                            Ok(PolicyMsg::AccountUpdated { field }) => {
                                push_toast_to(
                                    toast_sig,
                                    ToastKind::Info,
                                    &format!("Your {} was updated on another device.", field),
                                );
                            }
                            Ok(PolicyMsg::SecurityAlert { message }) => {
                                push_toast_to(toast_sig, ToastKind::Warning, &message);
                            }
                            Ok(PolicyMsg::ToastBroadcast { kind, message }) => {
                                let k = match kind.as_str() {
                                    "error"   => ToastKind::Error,
                                    "warning" => ToastKind::Warning,
                                    _         => ToastKind::Info,
                                };
                                push_toast_to(toast_sig, k, &message);
                            }
                            Ok(PolicyMsg::Other) | Err(_) => {}
                        }
                    }
                    Err(e) => {
                        web_sys::console::warn_1(&format!("policy WS: error {:?}", e).into());
                        break;
                    }
                    _ => {}
                }
            }

            reconnect_delay().await;
        }
    });
}

/// Returns true if `path` is covered by any entry in `deny_paths`.
///
/// Pattern semantics mirror `build_config::rbac_config::load_rbac_anonymous_deny_patterns`:
/// - `"/admin/*"` matches `"/admin"` and any sub-path like `"/admin/users"`
/// - `"/login"` matches only the exact path `"/login"`
pub fn requires_auth_runtime(path: &str, deny_paths: &[String]) -> bool {
    deny_paths.iter().any(|pattern| {
        if let Some(prefix) = pattern.strip_suffix("/*") {
            path == prefix || path.starts_with(&format!("{}/", prefix))
        } else {
            path == pattern
        }
    })
}

/// Build the WebSocket URL from the current page origin.
#[cfg(target_arch = "wasm32")]
fn ws_url(path: &str) -> String {
    let host = web_sys::window()
        .and_then(|w| w.location().host().ok())
        .unwrap_or_default();
    let protocol = web_sys::window()
        .and_then(|w| w.location().protocol().ok())
        .map(|p| if p == "https:" { "wss" } else { "ws" })
        .unwrap_or("ws");
    format!("{}://{}{}", protocol, host, path)
}

/// 3-second reconnect delay using `gloo_timers`.
#[cfg(target_arch = "wasm32")]
async fn reconnect_delay() {
    gloo_timers::future::TimeoutFuture::new(3_000).await;
}
