//! [`RwFileGuard`] — the [`FileGuard`] adapter (lot C6).
//!
//! A reader/writer lock **per [`GuardedResource`]**, backed by one
//! [`tokio::sync::RwLock`] per resource (created lazily and shared via `Arc`). N
//! concurrent readers **or** one exclusive writer per resource; different resources
//! are independent (their locks are distinct).
//!
//! ## Pure lock — no authorization here (cadrage « orchestrateur du projet », Alt. A)
//!
//! This adapter is a **pure serialisation primitive**: `acquire_write` never returns
//! [`GuardError::Forbidden`]. The single-writer *authorization* of the global
//! [`GuardedResource::ProjectContext`] (« only the orchestrator may write it directly,
//! everyone else proposes ») is **not** a concurrency concern and no longer lives in
//! the guard. It is decided one layer up, in the `ProposeContext` use case, which
//! consults the project's `OrchestratorDesignation` (now that the orchestrator can be
//! a *designated agent*, not just the human, the guard — which only sees a
//! [`ConversationParty`] — could not make that call coherently anyway). The guard's
//! sole job is to make readers/writers of the same resource take turns.
//!
//! ## Cooperative scope (cadrage §9.5)
//!
//! This guard serialises access **inside the IdeA path** (the MCP tools / use cases).
//! It is **cooperative**: it does not sandbox an agent that keeps a raw shell — real
//! airtightness (revoking raw fs access to these paths) is an OS-sandbox concern
//! (Landlock) and is **out of scope** here.
//!
//! ## Concurrency
//!
//! The resource → lock registry lives behind a **synchronous** [`Mutex`], held only
//! for the O(1) get-or-insert of an `Arc<RwLock<()>>` and **never across an
//! `.await`**. The actual wait happens on the tokio `RwLock`'s `owned` guard, whose
//! lifetime is `'static` (it owns its `Arc`), so it can be boxed into the domain's
//! RAII [`ReadLease`]/[`WriteLease`].

use std::collections::HashMap;
use std::sync::{Arc, Mutex};

use async_trait::async_trait;
use tokio::sync::RwLock;

use domain::conversation::ConversationParty;
use domain::fileguard::{FileGuard, GuardError, GuardedResource, ReadLease, WriteLease};

/// The [`FileGuard`] adapter: one reader/writer lock per guarded resource.
///
/// Held at the composition root as `Arc<dyn FileGuard>`; cloneable bookkeeping is
/// interior (the registry is a shared `Mutex<HashMap<…>>`).
#[derive(Debug, Default)]
pub struct RwFileGuard {
    /// Lazily-created per-resource locks. The unit `()` payload is irrelevant — the
    /// lock's *mode* (read vs write) is the whole point; the lease only proves the
    /// slot is held until drop.
    locks: Mutex<HashMap<GuardedResource, Arc<RwLock<()>>>>,
}

impl RwFileGuard {
    /// Builds an empty guard (no resource is contended until first acquired).
    #[must_use]
    pub fn new() -> Self {
        Self::default()
    }

    /// Returns the shared lock for `res`, creating it on first use. The registry
    /// mutex is held only for this O(1) get-or-insert, never across an `.await`.
    fn lock_for(&self, res: &GuardedResource) -> Arc<RwLock<()>> {
        let mut registry = self
            .locks
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner);
        registry
            .entry(res.clone())
            .or_insert_with(|| Arc::new(RwLock::new(())))
            .clone()
    }
}

#[async_trait]
impl FileGuard for RwFileGuard {
    async fn acquire_read(
        &self,
        _who: ConversationParty,
        res: GuardedResource,
    ) -> Result<ReadLease, GuardError> {
        // Reading is never forbidden (cooperative contract): serialise behind any
        // in-flight writer of the same resource, then hand back the RAII lease.
        let lock = self.lock_for(&res);
        let guard = lock.read_owned().await;
        Ok(ReadLease::new(Box::new(guard)))
    }

    async fn acquire_write(
        &self,
        _who: ConversationParty,
        res: GuardedResource,
    ) -> Result<WriteLease, GuardError> {
        // Pure lock (Alt. A): no authorization here — `ProposeContext` decides who may
        // write the global context. Serialise behind any in-flight readers/writer of
        // the same resource, then hand back the exclusive RAII lease.
        let lock = self.lock_for(&res);
        let guard = lock.write_owned().await;
        Ok(WriteLease::new(Box::new(guard)))
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use domain::ids::AgentId;
    use domain::memory::MemorySlug;
    use std::sync::atomic::{AtomicUsize, Ordering};
    use std::time::Duration;

    fn agent_party(n: u128) -> ConversationParty {
        ConversationParty::agent(AgentId::from_uuid(uuid::Uuid::from_u128(n)))
    }

    fn mem(slug: &str) -> GuardedResource {
        GuardedResource::Memory(MemorySlug::new(slug).unwrap())
    }

    #[tokio::test]
    async fn many_readers_share_one_resource_concurrently() {
        let guard = RwFileGuard::new();
        // Three read leases held *at once* on the same resource must all be granted.
        let a = guard
            .acquire_read(ConversationParty::User, mem("note"))
            .await
            .unwrap();
        let b = guard
            .acquire_read(agent_party(1), mem("note"))
            .await
            .unwrap();
        let c = guard
            .acquire_read(agent_party(2), mem("note"))
            .await
            .unwrap();
        // If reads were exclusive this would have deadlocked above; reaching here is
        // the proof. Keep the leases alive until now.
        drop((a, b, c));
    }

    #[tokio::test]
    async fn writer_excludes_other_writers_serialising_them() {
        let guard = Arc::new(RwFileGuard::new());
        let counter = Arc::new(AtomicUsize::new(0));
        let observed_max = Arc::new(AtomicUsize::new(0));

        let mut handles = Vec::new();
        for _ in 0..8 {
            let guard = Arc::clone(&guard);
            let counter = Arc::clone(&counter);
            let observed_max = Arc::clone(&observed_max);
            handles.push(tokio::spawn(async move {
                let _lease = guard
                    .acquire_write(ConversationParty::User, mem("shared"))
                    .await
                    .unwrap();
                // While holding the write lease, at most one task may be inside.
                let inside = counter.fetch_add(1, Ordering::SeqCst) + 1;
                observed_max.fetch_max(inside, Ordering::SeqCst);
                tokio::time::sleep(Duration::from_millis(5)).await;
                counter.fetch_sub(1, Ordering::SeqCst);
            }));
        }
        for h in handles {
            h.await.unwrap();
        }
        assert_eq!(
            observed_max.load(Ordering::SeqCst),
            1,
            "a write lease must be exclusive — never two writers inside at once"
        );
    }

    #[tokio::test]
    async fn pure_lock_grants_project_context_write_to_any_party() {
        // Alt. A: the guard is a pure lock — it no longer enforces the single-writer
        // *authorization* (that moved into `ProposeContext`). Both the human and an
        // arbitrary agent obtain a write lease on the project context; `Forbidden` is
        // never produced at this layer.
        let guard = RwFileGuard::new();
        assert!(guard
            .acquire_write(ConversationParty::User, GuardedResource::ProjectContext)
            .await
            .is_ok());
        assert!(guard
            .acquire_write(agent_party(1), GuardedResource::ProjectContext)
            .await
            .is_ok());
    }

    #[tokio::test]
    async fn write_lease_releases_on_drop_letting_the_next_writer_in() {
        let guard = Arc::new(RwFileGuard::new());
        {
            let _lease = guard
                .acquire_write(ConversationParty::User, mem("r"))
                .await
                .unwrap();
            // Held here; a concurrent writer would block.
        } // <- RAII release at end of scope.

        // After the scope, a fresh writer acquires immediately (bounded wait).
        let again = tokio::time::timeout(
            Duration::from_millis(200),
            guard.acquire_write(ConversationParty::User, mem("r")),
        )
        .await
        .expect("lease must have released on drop")
        .unwrap();
        drop(again);
    }

    #[tokio::test]
    async fn distinct_resources_do_not_block_each_other() {
        let guard = RwFileGuard::new();
        let _w1 = guard
            .acquire_write(ConversationParty::User, mem("alpha"))
            .await
            .unwrap();
        // A writer on a *different* resource is independent — must not block.
        let w2 = tokio::time::timeout(
            Duration::from_millis(200),
            guard.acquire_write(ConversationParty::User, mem("beta")),
        )
        .await
        .expect("independent resources must not contend")
        .unwrap();
        drop(w2);
    }
}
