use std::path::PathBuf;
use std::sync::Arc;

use notify::{Config, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
use tokio::sync::{broadcast, RwLock};

use super::file_config::{FileRbacConfig, RbacOutcome, ResourceKind};
use crate::content::websocket::ContentNotification;

/// Watch `path` for modifications and atomically reload the shared `config`.
///
/// Spawns a `notify` watcher on `path` (non-recursive). Any `Modify` event
/// triggers a `FileRbacConfig::load` attempt; on success the lock is updated
/// and `policy_tx` receives a `RbacPolicyUpdated` broadcast so WASM clients
/// can sync without a page reload.
///
/// `deny_cache` is an optional shared cache of the current deny paths. When
/// provided, it is updated atomically alongside the config so new WS clients
/// get an accurate initial policy on connect.
///
/// The future completes only when the mpsc sender is dropped (i.e. the watcher
/// is dead), which normally never happens during a server's lifetime.
pub async fn watch_file_rbac_with_cache(
    path: PathBuf,
    config: Arc<RwLock<FileRbacConfig>>,
    policy_tx: broadcast::Sender<ContentNotification>,
    deny_cache: Option<Arc<RwLock<Vec<String>>>>,
) -> notify::Result<()> {
    let (tx, mut rx) = tokio::sync::mpsc::channel::<()>(8);

    let mut watcher = RecommendedWatcher::new(
        move |res: notify::Result<Event>| {
            if let Ok(ev) = res {
                if matches!(ev.kind, EventKind::Modify(_)) {
                    // blocking_send is fine here: the channel has capacity 8
                    // and the async consumer always drains it promptly.
                    let _ = tx.blocking_send(());
                }
            }
        },
        Config::default(),
    )?;

    watcher.watch(&path, RecursiveMode::NonRecursive)?;

    while rx.recv().await.is_some() {
        match FileRbacConfig::load(&path) {
            Ok(new_cfg) => {
                tracing::info!(
                    path = %path.display(),
                    groups = new_cfg.groups.len(),
                    user_overrides = new_cfg.user_overrides.len(),
                    "rbac.ncl reloaded — new policy active"
                );
                let deny_paths = extract_anonymous_deny_paths(&new_cfg);
                *config.write().await = new_cfg;
                if let Some(ref cache) = deny_cache {
                    *cache.write().await = deny_paths.clone();
                }
                let _ = policy_tx.send(ContentNotification::rbac_policy_updated(deny_paths));
            }
            Err(e) => {
                tracing::error!(
                    path = %path.display(),
                    error = %e,
                    "rbac.ncl reload failed — previous policy kept"
                );
            }
        }
    }

    Ok(())
}

/// Extract URL patterns denied for anonymous users from a loaded
/// `FileRbacConfig`.
///
/// Mirrors `build_config::rbac_config::load_rbac_anonymous_deny_patterns` but
/// operates on the runtime struct (no Nickel re-evaluation needed).
pub fn extract_anonymous_deny_paths(cfg: &FileRbacConfig) -> Vec<String> {
    cfg.defaults
        .unauthenticated_allow
        .iter()
        .filter(|r| r.resource == ResourceKind::Page && r.outcome == RbacOutcome::Deny)
        .map(|r| r.pattern.clone())
        .collect()
}
