//! Per-`case_dir` async mutex registry.
//!
//! Serializes the read-modify-write sequence around `oneliner.json`
//! so that a manual override (PUT handler) cannot race with a concurrent
//! LLM-driven auto-regeneration (`update_oneliner` in `transcribe::worker`).
//!
//! Concretely: both code paths take `OnelinerLocks::lock_for(case_dir)`
//! before reading-then-writing the state. Different case dirs use
//! independent inner mutexes, so unrelated cases never block each other.
//!
//! The registry is intentionally never pruned. Inner mutex entries are
//! tiny (`Arc<Mutex<()>>`) and the case-dir set per process is bounded
//! by realistic medical workloads (low thousands within the rolling
//! window). If this ever becomes a memory concern, weak references or
//! a periodic sweeper can be added without touching call sites.

use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;

use tokio::sync::{Mutex, OwnedMutexGuard};

/// Newtype wrapper around the per-`case_dir` mutex registry.
///
/// Cloning is cheap (`Arc` clone). Ships in `AppState` as a regular
/// field; handlers receive it via `State<OnelinerLocks>` thanks to a
/// `FromRef<AppState>` impl on the parent state.
#[derive(Clone, Default)]
pub struct OnelinerLocks(Arc<Mutex<HashMap<PathBuf, Arc<Mutex<()>>>>>);

impl OnelinerLocks {
    pub fn new() -> Self {
        Self::default()
    }

    /// Acquire the lock guarding `case_dir`'s `oneliner.json` read-modify-write
    /// sequence. The returned guard owns its own `Arc` to the inner mutex,
    /// so callers may carry it across `await` points (PUT handler, worker
    /// re-check) without lifetime gymnastics.
    pub async fn lock_for(&self, case_dir: &Path) -> OwnedMutexGuard<()> {
        let inner = {
            let mut map = self.0.lock().await;
            map.entry(case_dir.to_path_buf())
                .or_insert_with(|| Arc::new(Mutex::new(())))
                .clone()
        };
        inner.lock_owned().await
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::time::Duration;
    use tokio::time::{Instant, timeout};

    #[tokio::test]
    async fn distinct_case_dirs_do_not_block() {
        let locks = OnelinerLocks::new();
        let path_a = Path::new("/tmp/case-a");
        let path_b = Path::new("/tmp/case-b");

        let _guard_a = locks.lock_for(path_a).await;
        // Holding guard_a must NOT prevent locking path_b.
        let result = timeout(Duration::from_millis(100), locks.lock_for(path_b)).await;
        assert!(
            result.is_ok(),
            "lock_for(path_b) should not be blocked by lock_for(path_a)"
        );
    }

    #[tokio::test]
    async fn same_case_dir_serializes() {
        let locks = OnelinerLocks::new();
        let path = Path::new("/tmp/case-c");

        let guard1 = locks.lock_for(path).await;

        // Second lock attempt on same path must wait.
        let locks_clone = locks.clone();
        let path_buf = path.to_path_buf();
        let waiter = tokio::spawn(async move {
            let start = Instant::now();
            let _g2 = locks_clone.lock_for(&path_buf).await;
            start.elapsed()
        });

        // Yield once to let the spawned task try to acquire and park.
        tokio::time::sleep(Duration::from_millis(50)).await;
        drop(guard1);

        let elapsed = waiter.await.expect("waiter task panicked");
        assert!(
            elapsed >= Duration::from_millis(40),
            "second lock should have waited at least ~50ms, waited {elapsed:?}"
        );
    }

    #[tokio::test]
    async fn registry_reuses_inner_mutex_across_calls() {
        let locks = OnelinerLocks::new();
        let path = Path::new("/tmp/case-d");

        // Take and release; second lock must hit the same inner mutex
        // (i.e. registry persists the entry).
        {
            let _g = locks.lock_for(path).await;
        }
        let map_len_after_first = locks.0.lock().await.len();
        let _g = locks.lock_for(path).await;
        let map_len_after_second = locks.0.lock().await.len();
        assert_eq!(map_len_after_first, 1);
        assert_eq!(map_len_after_second, 1);
    }
}
