//! Single declared `inotify` watch (ADR-0006 Phase F3 deliverable).
//!
//! Linux-only. We use raw libc (`inotify_init1`, `inotify_add_watch`,
//! non-blocking `read`) rather than a higher-level crate because:
//!   1. The probe is a single watch descriptor — no need for the orchestration
//!      a wrapper crate provides.
//!   2. Adding a dep here means risking a transitive pull on something the
//!      DENY list forbids (sha2-as-mac, etc.).
//!   3. The probe must remain trivially auditable; ~50 lines of libc fits in
//!      one screen.

#![cfg(target_os = "linux")]
#![allow(unsafe_code)] // libc::inotify_* have no safe wrapper at this layer.

use std::ffi::CString;
use std::io;

use crate::probes::now_monotonic_ns;
use crate::{probe_source, ProbeEvent};

/// One declared `inotify` watch on a path the spec named.
pub struct InotifyProbe {
    fd: libc::c_int,
    /// `comm` we report when the watch fires — usually the agent's own name.
    self_comm: String,
}

impl InotifyProbe {
    /// Open an inotify instance in non-blocking mode and add a single watch
    /// on `path` for `IN_MODIFY | IN_CREATE | IN_DELETE | IN_ATTRIB`.
    ///
    /// Errors propagate through `io::Error` so the caller (the binary
    /// entrypoint) can choose between `fail-cell` (default under hardened)
    /// and `degrade` per the spec's `onAgentFailure`.
    pub fn open(path: &str, self_comm: impl Into<String>) -> io::Result<Self> {
        // SAFETY: inotify_init1 is always safe to call; the IN_NONBLOCK |
        // IN_CLOEXEC flags are well-defined.
        let fd = unsafe { libc::inotify_init1(libc::IN_NONBLOCK | libc::IN_CLOEXEC) };
        if fd < 0 {
            return Err(io::Error::last_os_error());
        }

        let cpath =
            CString::new(path).map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?;
        let mask = libc::IN_MODIFY | libc::IN_CREATE | libc::IN_DELETE | libc::IN_ATTRIB;
        // SAFETY: `cpath` is a NUL-terminated C string with stable memory for
        // the duration of the call; `fd` is a valid inotify descriptor we
        // just opened.
        let wd = unsafe { libc::inotify_add_watch(fd, cpath.as_ptr(), mask as u32) };
        if wd < 0 {
            let err = io::Error::last_os_error();
            // SAFETY: close on a fd we own is always safe.
            unsafe { libc::close(fd) };
            return Err(err);
        }

        Ok(Self {
            fd,
            self_comm: self_comm.into(),
        })
    }

    /// Drain pending inotify events into [`ProbeEvent`]s. Non-blocking; if
    /// the kernel queue is empty, returns an empty vector.
    pub fn poll(&mut self) -> Vec<ProbeEvent> {
        let mut out = Vec::new();
        let mut buf = [0u8; 4096];
        loop {
            // SAFETY: `self.fd` is a valid fd we own; `buf` is a valid
            // mutable slice of the declared length.
            let n =
                unsafe { libc::read(self.fd, buf.as_mut_ptr() as *mut libc::c_void, buf.len()) };
            if n <= 0 {
                // EAGAIN/EWOULDBLOCK -> queue empty; any other error is
                // benign for the agent (we'll retry next poll).
                break;
            }
            // We don't decode the inotify_event records here — the host gets
            // a "watch fired" declaration with current pid/comm. Per
            // ADR-0006, attribution stays host-stamped; the path itself is
            // already in the spec.
            out.push(ProbeEvent {
                probe_source: probe_source::FS_INOTIFY_FIRED,
                guest_pid: std::process::id(),
                guest_comm: self.self_comm.clone(),
                guest_monotonic_ns: now_monotonic_ns(),
            });
            // One event per `read()` call — coalescing N kernel records into
            // 1 declared event is intentional. The host's interpretation is
            // "the declared watch fired at least once in this window".
        }
        out
    }
}

impl Drop for InotifyProbe {
    fn drop(&mut self) {
        if self.fd >= 0 {
            // SAFETY: close on a fd we own; idempotent at end-of-life.
            unsafe {
                libc::close(self.fd);
            }
        }
    }
}
