//! Wrapped FFI bindings to Linux-PAM
//!
//! Rustified wrappers around the unsafe PAM functions.

#[cfg(feature = "client")]
pub use appl::*;

#[cfg(feature = "module")]
pub use modules::*;

pub use types::*;

/// Linux-PAM and OpenPAM (FreeBSD, NetBSD, DragonFly BSD, macOS) all provide
/// pam_misc.h with pam_misc_paste_env, pam_misc_setenv, pam_misc_drop_env.
#[cfg(any(
    target_os = "linux",
    target_os = "freebsd",
    target_os = "netbsd",
    target_os = "macos",
    target_os = "dragonfly"
))]
pub use misc::*;

/* ------------------------ <security/pam_appl.h> -------------------------- */
#[cfg(feature = "client")]
mod appl {
    use crate::{ffi, PamContext, PamFlags, PamHandle, PamResult, PamReturnCode};

    use libc::c_int;
    use std::ffi::CString;

    /// Create the PAM context and initiate the PAM transaction.
    ///
    /// Returns an owned `PamContext` wrapper instead of `&'a mut PamHandle`.
    /// The previous signature with an unconstrained lifetime `'a` allowed the
    /// caller to hold the reference beyond the handle's actual lifetime,
    /// enabling use-after-free and double-free after `pam_end`. See MED-05.
    #[inline]
    pub fn start(
        service: &str,
        user: Option<&str>,
        conversation: &ffi::pam_conv,
    ) -> PamResult<PamContext> {
        if let Ok(service) = CString::new(service) {
            let mut handle: *mut PamHandle = std::ptr::null_mut();

            // CString must live until pam_start returns to avoid dangling pointer.
            // See security audit CRIT-01: try_str_option_to_ptr returned a dangling ptr.
            let user_cstring = match user {
                Some(u) => Some(CString::new(u).map_err(|_| PamReturnCode::Buf_Err)?),
                None => None,
            };
            let user_ptr = user_cstring
                .as_ref()
                .map_or(std::ptr::null(), |c| c.as_ptr());

            match unsafe { ffi::pam_start(service.as_ptr(), user_ptr, conversation, &mut handle) }
                .into()
            {
                PamReturnCode::Success => {
                    if handle.is_null() {
                        return Err(PamReturnCode::System_Err.into());
                    }
                    // Safety: handle is non-null and valid from pam_start.
                    Ok(unsafe { PamContext::from_ptr(handle) })
                }
                err => Err(err.into()),
            }
        } else {
            super::buffer_error()
        }
    }

    /// Create the PAM context using an alternative configuration directory
    /// instead of `/etc/pam.d` (Linux-PAM extension).
    ///
    /// Mirrors `start()` but accepts a `confdir` path pointing to a directory
    /// with a PAM stack configuration. Useful for:
    /// - Isolated integration testing without touching system PAM config
    /// - Sandboxing / containers with custom PAM policies
    ///
    /// Only available on Linux.
    #[cfg(target_os = "linux")]
    #[inline]
    pub fn start_confdir(
        service: &str,
        user: Option<&str>,
        conversation: &ffi::pam_conv,
        confdir: &str,
    ) -> PamResult<PamContext> {
        if let (Ok(service), Ok(confdir)) = (CString::new(service), CString::new(confdir)) {
            let mut handle: *mut PamHandle = std::ptr::null_mut();

            let user_cstring = match user {
                Some(u) => Some(CString::new(u).map_err(|_| PamReturnCode::Buf_Err)?),
                None => None,
            };
            let user_ptr = user_cstring
                .as_ref()
                .map_or(std::ptr::null(), |c| c.as_ptr());

            match unsafe {
                ffi::pam_start_confdir(
                    service.as_ptr(),
                    user_ptr,
                    conversation,
                    confdir.as_ptr(),
                    &mut handle,
                )
            }
            .into()
            {
                PamReturnCode::Success => {
                    if handle.is_null() {
                        return Err(PamReturnCode::System_Err.into());
                    }
                    Ok(unsafe { PamContext::from_ptr(handle) })
                }
                err => Err(err.into()),
            }
        } else {
            super::buffer_error()
        }
    }

    /// Terminate the PAM transaction.
    ///
    /// Passes `PAM_DATA_SILENT` to suppress module data cleanup callbacks
    /// registered via `pam_set_data`. The caller (typically `Client::drop`)
    /// manages its own cleanup (close_session, delete credentials, zeroize),
    /// and suppressing PAM's internal cleanup prevents double-free or
    /// use-after-free of memory already released by the Rust side.
    ///
    /// This function must be called last in the PAM context.
    #[inline]
    pub fn end(handle: &mut PamHandle, status: PamReturnCode) -> PamReturnCode {
        let flags = status as c_int | crate::ffi::PAM_DATA_SILENT as c_int;
        unsafe { ffi::pam_end(handle, flags) }.into()
    }

    /// Authenticate the user via the `Conversation` passed to `start`
    ///
    /// Valid flags: `SILENT`, `DISALLOW_NULL_AUTHTOK`, or combinations via `|`.
    #[inline]
    pub fn authenticate(handle: &mut PamHandle, flags: PamFlags) -> PamReturnCode {
        unsafe { ffi::pam_authenticate(handle, flags.bits()) }.into()
    }

    /// Modify the credentials of the user associated with the PAM transaction
    ///
    /// This function should be called after the user has been authenticated and
    /// before a session is opened.
    ///
    /// Valid flags: `SILENT`, `ESTABLISH_CRED`, `DELETE_CRED`, `REINITIALIZE_CRED`,
    /// `REFRESH_CRED`, or combinations via `|`.
    #[inline]
    pub fn setcred(handle: &mut PamHandle, flags: PamFlags) -> PamReturnCode {
        unsafe { ffi::pam_setcred(handle, flags.bits()) }.into()
    }

    /// Determine if the user's account is valid
    ///
    /// This function is typically called after a user has been authenticated.
    ///
    /// Valid flags: `SILENT`, `DISALLOW_NULL_AUTHTOK`, or combinations via `|`.
    #[inline]
    pub fn acct_mgmt(handle: &mut PamHandle, flags: PamFlags) -> PamReturnCode {
        unsafe { ffi::pam_acct_mgmt(handle, flags.bits()) }.into()
    }

    /// Set up a user session for a previously authenticated user
    #[inline]
    pub fn open_session(handle: &mut PamHandle, silent: bool) -> PamReturnCode {
        let flag = silent as c_int;
        unsafe { ffi::pam_open_session(handle, flag) }.into()
    }

    /// Indicate that an authenticated user session has ended
    #[inline]
    pub fn close_session(handle: &mut PamHandle, silent: bool) -> PamReturnCode {
        let flag = silent as c_int;
        unsafe { ffi::pam_close_session(handle, flag) }.into()
    }

    /// Change the authentication token for the user associated with the PAM
    /// transaction
    ///
    /// Valid flags: `SILENT`, `CHANGE_EXPIRED_AUTHTOK`, or combinations via `|`.
    #[inline]
    pub fn chauthtok(handle: &mut PamHandle, flags: PamFlags) -> PamReturnCode {
        unsafe { ffi::pam_chauthtok(handle, flags.bits()) }.into()
    }

    /// Set the delay for failed authentication attempts (brute-force mitigation).
    ///
    /// PAM inserts a random delay (up to ±50% of `usec` microseconds) before
    /// returning an authentication failure. This is the native PAM mechanism
    /// against timing-based user enumeration and brute-force attacks.
    /// Applications can call this before `authenticate()` to set a baseline
    /// delay, or after each failed attempt to implement exponential backoff.
    ///
    /// Recommended default: `fail_delay(handle, 2_000_000)` (2 seconds).
    #[inline]
    pub fn fail_delay(handle: &mut PamHandle, usec: u32) -> PamReturnCode {
        unsafe { ffi::pam_fail_delay(handle, usec as libc::c_uint) }.into()
    }
}
/* ------------------------ <security/pam_appl.h> -------------------------- */

/* ----------------------- <security/_pam_types.h> ------------------------- */
mod types {
    use crate::{env, ffi, PamHandle, PamItemType, PamResult, PamReturnCode};

    use libc::{c_char, c_int, c_void};
    use std::ffi::{CStr, CString};

    /// Update PAM information of type `item_type` in the associated PAM transaction
    #[inline]
    pub fn set_item(
        handle: &mut PamHandle,
        item_type: PamItemType,
        item: &c_void,
    ) -> PamResult<()> {
        match unsafe { ffi::pam_set_item(handle, item_type as c_int, item) }.into() {
            PamReturnCode::Success => Ok(()),
            err => Err(err.into()),
        }
    }

    /// Retrieve PAM information of type `item_type` from the associated PAM transaction.
    ///
    /// The returned reference is tied to the lifetime of the `handle` borrow to
    /// prevent dangling references if PAM invalidates the item (e.g., via a
    /// subsequent `pam_set_item`). See MED-04.
    #[inline]
    pub fn get_item<'a>(handle: &'a PamHandle, item_type: PamItemType) -> PamResult<&'a c_void> {
        let mut item_ptr: *const c_void = std::ptr::null();
        match unsafe { ffi::pam_get_item(handle, item_type as c_int, &mut item_ptr) }.into() {
            PamReturnCode::Success => {
                if item_ptr.is_null() {
                    return Err(PamReturnCode::System_Err.into());
                }
                Ok(unsafe { &*item_ptr })
            }
            err => Err(err.into()),
        }
    }

    /// Retrieve a `CStr` describing the `PamReturnCode` passed, potentially
    /// using LC_MESSAGES to localize the result.
    ///
    /// Returns a lossy String to tolerate non-UTF-8 output from PAM's
    /// localization (MED-01).
    #[inline]
    pub fn strerror(handle: &mut PamHandle, errnum: PamReturnCode) -> String {
        // We don't match here, as man says this function always returns a pointer to a string
        unsafe { CStr::from_ptr(ffi::pam_strerror(handle, errnum as c_int)) }
            .to_string_lossy()
            .into_owned()
    }

    /// Add or change PAM environment variables associated with the PAM transaction
    #[inline]
    pub fn putenv(handle: &mut PamHandle, name_value: &str) -> PamResult<()> {
        if let Ok(name_value) = CString::new(name_value) {
            match unsafe { ffi::pam_putenv(handle, name_value.as_ptr()) }.into() {
                PamReturnCode::Success => Ok(()),
                err => Err(err.into()),
            }
        } else {
            super::buffer_error()
        }
    }

    /// Get the value of a PAM environment variable associated with the PAM
    /// transaction.
    ///
    /// Returns a lossy String to tolerate non-UTF-8 values from PAM (MED-01).
    #[inline]
    pub fn getenv<'a>(handle: &'a mut PamHandle, name: &str) -> PamResult<Option<String>> {
        if let Ok(name) = CString::new(name) {
            // Get environment variable
            let env = unsafe { ffi::pam_getenv(handle, name.as_ptr()) };
            if !env.is_null() {
                // Convert to rust String with lossy UTF-8 handling
                Ok(Some(
                    unsafe { CStr::from_ptr(env) }
                        .to_string_lossy()
                        .into_owned(),
                ))
            } else {
                // This might still be an error, but we don't know for sure
                Ok(None)
            }
        } else {
            super::buffer_error()
        }
    }

    // FIXME: Implement this properly
    /// Retrieve a complee copy of the PAM environment associated with
    /// the PAM transaction
    #[inline]
    pub fn getenvlist(handle: &mut PamHandle) -> env::PamEnvList {
        let ptr = unsafe { ffi::pam_getenvlist(handle) };
        env::PamEnvList::from_ptr(ptr as *const *const c_char)
    }

    /// Set a string-typed PAM item (RHost, TTY, RUser, Service, User).
    ///
    /// Internal helper for the typed convenience wrappers below.
    /// Creates a `CString` from `value` and calls `pam_set_item`.
    #[inline]
    fn set_item_str(handle: &mut PamHandle, item_type: PamItemType, value: &str) -> PamResult<()> {
        let c_value = CString::new(value).map_err(|_| PamReturnCode::Buf_Err)?;
        // Safety: c_value lives until the FFI call returns
        let void_ptr: &c_void = unsafe { &*(c_value.as_ptr() as *const c_void) };
        set_item(handle, item_type, void_ptr)
    }

    /// Set the remote host (PAM_RHOST) for the current transaction.
    ///
    /// Required by pam_access, pam_faillock, and audit modules that
    /// restrict or log access based on origin.
    #[inline]
    pub fn set_rhost(handle: &mut PamHandle, rhost: &str) -> PamResult<()> {
        set_item_str(handle, PamItemType::RHost, rhost)
    }

    /// Set the terminal name (PAM_TTY) for the current transaction.
    #[inline]
    pub fn set_tty(handle: &mut PamHandle, tty: &str) -> PamResult<()> {
        set_item_str(handle, PamItemType::TTY, tty)
    }

    /// Set the requesting user name (PAM_RUSER) for the current transaction.
    #[inline]
    pub fn set_ruser(handle: &mut PamHandle, ruser: &str) -> PamResult<()> {
        set_item_str(handle, PamItemType::RUser, ruser)
    }
}
/* ----------------------- <security/_pam_types.h> ------------------------- */

/* ----------------------- <security/pam_misc.h> --------------------------- */
/// OpenPAM (FreeBSD, NetBSD, DragonFly BSD, macOS) provides the same pam_misc.h API
/// as Linux-PAM. The FIXME about other platforms is resolved: these are the platforms.
#[cfg(any(
    target_os = "linux",
    target_os = "freebsd",
    target_os = "netbsd",
    target_os = "macos",
    target_os = "dragonfly"
))]
mod misc {
    use crate::{ffi, PamHandle, PamResult, PamReturnCode};

    use std::ffi::CString;

    /// Update the PAM environment via the supplied list
    #[inline]
    pub fn misc_paste_env(handle: &mut PamHandle, user_env: &[&str]) -> PamResult<()> {
        // Convert &str slices to CStrings, returning Buf_Err on interior nul bytes
        // instead of panicking (MED-06).
        let user_env: Vec<CString> = user_env
            .iter()
            .map(|&env| CString::new(env).map_err(|_| PamReturnCode::Buf_Err))
            .collect::<Result<_, _>>()?;
        let env_ptrs: Vec<_> = user_env
            .iter()
            .map(|env| env.as_ptr())
            .chain(Some(std::ptr::null()))
            .collect();

        match unsafe { ffi::pam_misc_paste_env(handle, env_ptrs.as_ptr()) }.into() {
            PamReturnCode::Success => Ok(()),
            err => Err(err.into()),
        }
    }

    // FIXME: implement this properly
    /*/// Free memory of an environment list obtained via `getenvlist`
    #[inline]
    pub fn misc_drop_env(env: &mut *mut c_char) -> PamReturnCode {
        unsafe { ffi::pam_misc_drop_env(env) })
    }*/

    /// Add or change PAM environment variables associated with the PAM transaction
    /// in BSD style "name=value"
    #[inline]
    pub fn misc_setenv(
        handle: &mut PamHandle,
        name: &str,
        value: &str,
        readonly: bool,
    ) -> PamResult<()> {
        if let (Ok(name), Ok(value)) = (CString::new(name), CString::new(value)) {
            let flag = readonly as libc::c_int;
            match unsafe { ffi::pam_misc_setenv(handle, name.as_ptr(), value.as_ptr(), flag) }
                .into()
            {
                PamReturnCode::Success => Ok(()),
                err => Err(err.into()),
            }
        } else {
            super::buffer_error()
        }
    }
}
/* ----------------------- <security/pam_misc.h> --------------------------- */

/* ----------------------- <security/pam_modules.h> ------------------------ */
#[cfg(feature = "module")]
mod modules {
    use crate::{ffi, PamHandle, PamResult, PamReturnCode};

    use libc::{c_char, c_int, c_void};
    use std::ffi::{CStr, CString};

    /// Associate a pointer to an object with the given `module_data_name` in
    /// the current PAM context
    #[inline]
    pub fn set_data(
        handle: &mut PamHandle,
        module_data_name: &str,
        data: &mut c_void,
        // FIXME: Remove bare ptrs from closure signature
        cleanup: Option<unsafe extern "C" fn(*mut PamHandle, *mut c_void, c_int)>,
    ) -> PamResult<()> {
        if let Ok(module_data_name) = CString::new(module_data_name) {
            match unsafe { ffi::pam_set_data(handle, module_data_name.as_ptr(), data, cleanup) }
                .into()
            {
                PamReturnCode::Success => Ok(()),
                err => Err(err.into()),
            }
        } else {
            super::buffer_error()
        }
    }

    // FIXME: implement this properly
    /*/// Retrieve the object associated with the given `module_data_name` from
    /// the current PAM context
    //pub fn get_data(handle: *const PamHandle, module_data_name: *const c_char, data: *const *const c_void);
     */

    /// Return the name of the user as specified via `start`.
    ///
    /// Returns a lossy String to tolerate non-UTF-8 usernames from PAM (MED-01).
    #[inline]
    pub fn get_user<'a>(handle: &'a PamHandle, prompt: Option<&str>) -> PamResult<String> {
        // WORKAROUND: bindgen (used by pam-sys) generates pam_get_user with a
        // *mut pam_handle_t parameter, but the Linux-PAM spec (pam_get_user(3))
        // declares the handle as const. The function does not write through the
        // pointer — it only reads. This cast is safe per the C contract.
        //
        // TODO: report upstream at https://github.com/1wilkens/pam-sys to
        // correct the bindgen const-ness annotation so this cast can be removed.
        let handle = handle as *const PamHandle as *mut PamHandle;
        let mut user_ptr: *const c_char = std::ptr::null();

        // CString must live until pam_get_user returns to avoid dangling pointer.
        // See security audit CRIT-01: try_str_option_to_ptr returned a dangling ptr.
        let prompt_cstring = match prompt {
            Some(p) => Some(CString::new(p).map_err(|_| PamReturnCode::Buf_Err)?),
            None => None,
        };
        let prompt_ptr = prompt_cstring
            .as_ref()
            .map_or(std::ptr::null(), |c| c.as_ptr());

        match unsafe { ffi::pam_get_user(handle, &mut user_ptr, prompt_ptr) }.into() {
            PamReturnCode::Success => {
                if user_ptr.is_null() {
                    return Err(PamReturnCode::System_Err.into());
                }
                Ok(unsafe { CStr::from_ptr(user_ptr) }
                    .to_string_lossy()
                    .into_owned())
            }
            err => Err(err.into()),
        }
    }
}
/* ----------------------- <security/pam_modules.h> ------------------------ */

#[inline]
fn buffer_error<T>() -> crate::PamResult<T> {
    Err(crate::PamReturnCode::Buf_Err.into())
}
