//! Invalidation queue.
//!
//! Mirrors the TS kernel's `pending` / `pendingScoped` / `flush()` pair
//! at `frontends/mizan-base/src/index.ts`. Mutations accumulate
//! invalidation targets; the queue batches them and triggers refetches
//! on the matching context entries.
//!
//! The TS kernel uses `queueMicrotask(flush)` to batch within a single
//! event-loop tick. The Rust equivalent is a `tokio::task::yield_now()`
//! debounce: when `invalidate()` is called, push to the queue, and if
//! no flush is scheduled spawn a task that yields once then flushes.
//! That gives the same "batch within a single async tick" semantics.

use std::collections::HashSet;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};

use serde_json::Value;
use tokio::sync::Mutex;

use crate::context::ContextRegistry;


#[derive(Debug, Clone)]
pub struct ScopedTarget {
    pub context: String,
    pub params: Value,
}


#[derive(Default)]
struct Pending {
    broad: HashSet<String>,
    scoped: Vec<ScopedTarget>,
}


pub struct InvalidationQueue {
    pending: Mutex<Pending>,
    scheduled: AtomicBool,
    registry: Arc<ContextRegistry>,
}


impl InvalidationQueue {
    pub fn new(registry: Arc<ContextRegistry>) -> Arc<Self> {
        Arc::new(Self {
            pending: Mutex::new(Pending::default()),
            scheduled: AtomicBool::new(false),
            registry,
        })
    }

    /// Schedule a broad invalidation (every entry of `name` refetches).
    pub async fn invalidate(self: &Arc<Self>, name: impl Into<String>) {
        {
            let mut pending = self.pending.lock().await;
            pending.broad.insert(name.into());
        }
        self.schedule_flush();
    }

    /// Schedule a scoped invalidation (the entry matching `(name,
    /// params)` refetches).
    pub async fn invalidate_scoped(self: &Arc<Self>, name: impl Into<String>, params: Value) {
        {
            let mut pending = self.pending.lock().await;
            pending.scoped.push(ScopedTarget { context: name.into(), params });
        }
        self.schedule_flush();
    }

    fn schedule_flush(self: &Arc<Self>) {
        if self.scheduled.swap(true, Ordering::SeqCst) {
            return;
        }
        let this = Arc::clone(self);
        tokio::spawn(async move {
            // Yield once to batch invalidations queued in the same
            // async tick — equivalent to TS `queueMicrotask`.
            tokio::task::yield_now().await;
            this.flush().await;
            this.scheduled.store(false, Ordering::SeqCst);
        });
    }

    async fn flush(&self) {
        let snapshot = {
            let mut pending = self.pending.lock().await;
            let broad = std::mem::take(&mut pending.broad);
            let scoped = std::mem::take(&mut pending.scoped);
            (broad, scoped)
        };
        let (broad, scoped) = snapshot;

        // Broad first — they cover all scoped variants of the same name.
        for name in &broad {
            self.registry.invalidate_broad(name).await;
        }
        for target in &scoped {
            if broad.contains(&target.context) {
                continue;
            }
            self.registry.invalidate_scoped(&target.context, &target.params).await;
        }
    }
}


#[cfg(test)]
mod tests {
    use super::*;
    use crate::context::{ContextHandle, ContextRegistry, ContextStatus, FetchFn};
    use serde_json::json;

    fn counted_fetch(counter: Arc<std::sync::atomic::AtomicU32>) -> FetchFn {
        Arc::new(move || {
            let counter = Arc::clone(&counter);
            Box::pin(async move {
                let n = counter.fetch_add(1, Ordering::SeqCst) + 1;
                Ok(json!({ "count": n }))
            })
        })
    }

    async fn wait_for_success(handle: &mut ContextHandle) {
        loop {
            handle.rx.changed().await.unwrap();
            if handle.state().status == ContextStatus::Success {
                return;
            }
        }
    }

    #[tokio::test]
    async fn broad_invalidate_triggers_refetch() {
        let registry = Arc::new(ContextRegistry::new());
        let queue = InvalidationQueue::new(Arc::clone(&registry));
        let counter = Arc::new(std::sync::atomic::AtomicU32::new(0));
        let mut handle = registry.register("user", json!({}), counted_fetch(Arc::clone(&counter)), None).await;
        handle.refetch();
        wait_for_success(&mut handle).await;
        assert_eq!(counter.load(Ordering::SeqCst), 1);
        queue.invalidate("user").await;
        wait_for_success(&mut handle).await;
        assert_eq!(counter.load(Ordering::SeqCst), 2);
    }
}
