//! Functions and structures that can be used to implement a PAM module
//!
//! Inspired by anowell/pam-rs

use crate::{PamHandle, PamReturnCode};
use std::ffi::CStr;
use std::os::raw::c_uint;

// FIXME: Find a solution for the flags containing ORed integers
#[allow(unused_variables)]
/// Trait representing a PAM module.
///
/// Modules should override the desired functions and call the macro `impl_pam_module`.
/// This exports the respective functions at the expected symbols prefixed with `pam_sm_`.
///
/// ```no_run
/// use pam_unix::{PamModule, export_pam_module};
///
/// pub struct MyModule;
/// impl PamModule for MyModule {}
///
/// // FIXME: Currently gets E0433: failed to resolve for `MyModule`
/// //export_pam_module!(MyModule);
/// ```
pub trait PamModule {
    fn account_management(handle: &PamHandle, args: Vec<&CStr>, flags: c_uint) -> PamReturnCode {
        PamReturnCode::Ignore
    }
    fn authenticate(handle: &PamHandle, args: Vec<&CStr>, flags: c_uint) -> PamReturnCode {
        PamReturnCode::Ignore
    }
    fn change_auth_token(handle: &PamHandle, args: Vec<&CStr>, flags: c_uint) -> PamReturnCode {
        PamReturnCode::Ignore
    }
    fn close_session(handle: &PamHandle, args: Vec<&CStr>, flags: c_uint) -> PamReturnCode {
        PamReturnCode::Ignore
    }
    fn open_session(handle: &PamHandle, args: Vec<&CStr>, flags: c_uint) -> PamReturnCode {
        PamReturnCode::Ignore
    }
    fn set_credentials(handle: &PamHandle, args: Vec<&CStr>, flags: c_uint) -> PamReturnCode {
        PamReturnCode::Ignore
    }
}

#[macro_export]
/// Export the given struct as a PAM module by wiring up the respective extern "C" functions
macro_rules! export_pam_module {
    ($struct:ident) => {
        pub use _pam_module_::*;
        mod _pam_module_ {
            use std::ffi::CStr;
            use std::os::raw::{c_char, c_int, c_uint};
            use $crate::{PamHandle, PamModule, PamReturnCode};

            /// Maximum number of PAM module arguments accepted.
            const MAX_MODULE_ARGS: c_int = 256;

            /// Convert raw `argc`/`argv` from the PAM stack into a safe `Vec<&CStr>`.
            fn convert_args<'a>(argc: c_int, argv: *const *const c_char) -> Vec<&'a CStr> {
                if argc <= 0 || argc > MAX_MODULE_ARGS || argv.is_null() {
                    return Vec::new();
                }
                (0..argc as isize)
                    .filter_map(|i| unsafe {
                        let ptr = *argv.offset(i);
                        if ptr.is_null() {
                            None
                        } else {
                            Some(CStr::from_ptr(ptr))
                        }
                    })
                    .collect()
            }

            /// Validate the raw PAM handle from C is non-null and convert to
            /// a Rust reference. Returns System_Err on null (defensive hardening
            /// — a well-formed PAM stack never passes null, but a buggy or
            /// malicious one could).
            unsafe fn try_ref_handle<'a>(
                handle: *const PamHandle,
            ) -> Result<&'a PamHandle, PamReturnCode> {
                if handle.is_null() {
                    Err(PamReturnCode::System_Err)
                } else {
                    Ok(&*handle)
                }
            }

            #[no_mangle]
            pub extern "C" fn pam_sm_acct_mgmt(
                handle: *const PamHandle,
                flags: c_uint,
                argc: c_int,
                argv: *const *const c_char,
            ) -> PamReturnCode {
                let args = convert_args(argc, argv);
                let handle = match unsafe { try_ref_handle(handle) } {
                    Ok(h) => h,
                    Err(e) => return e,
                };
                super::$struct::account_management(handle, args, flags)
            }
            #[no_mangle]
            pub extern "C" fn pam_sm_authenticate(
                handle: *const PamHandle,
                flags: c_uint,
                argc: c_int,
                argv: *const *const c_char,
            ) -> PamReturnCode {
                let args = convert_args(argc, argv);
                let handle = match unsafe { try_ref_handle(handle) } {
                    Ok(h) => h,
                    Err(e) => return e,
                };
                super::$struct::authenticate(handle, args, flags)
            }
            #[no_mangle]
            pub extern "C" fn pam_sm_chauthtok(
                handle: *const PamHandle,
                flags: c_uint,
                argc: c_int,
                argv: *const *const c_char,
            ) -> PamReturnCode {
                let args = convert_args(argc, argv);
                let handle = match unsafe { try_ref_handle(handle) } {
                    Ok(h) => h,
                    Err(e) => return e,
                };
                super::$struct::change_auth_token(handle, args, flags)
            }
            #[no_mangle]
            pub extern "C" fn pam_sm_close_session(
                handle: *const PamHandle,
                flags: c_uint,
                argc: c_int,
                argv: *const *const c_char,
            ) -> PamReturnCode {
                let args = convert_args(argc, argv);
                let handle = match unsafe { try_ref_handle(handle) } {
                    Ok(h) => h,
                    Err(e) => return e,
                };
                super::$struct::close_session(handle, args, flags)
            }
            #[no_mangle]
            pub extern "C" fn pam_sm_open_session(
                handle: *const PamHandle,
                flags: c_uint,
                argc: c_int,
                argv: *const *const c_char,
            ) -> PamReturnCode {
                let args = convert_args(argc, argv);
                let handle = match unsafe { try_ref_handle(handle) } {
                    Ok(h) => h,
                    Err(e) => return e,
                };
                super::$struct::open_session(handle, args, flags)
            }
            #[no_mangle]
            pub extern "C" fn pam_sm_setcred(
                handle: *const PamHandle,
                flags: c_uint,
                argc: c_int,
                argv: *const *const c_char,
            ) -> PamReturnCode {
                let args = convert_args(argc, argv);
                let handle = match unsafe { try_ref_handle(handle) } {
                    Ok(h) => h,
                    Err(e) => return e,
                };
                super::$struct::set_credentials(handle, args, flags)
            }
        }
    };
}

#[cfg(test)]
pub mod test {
    use super::PamModule;
    use crate::export_pam_module;

    pub struct TestModule;
    impl PamModule for TestModule {}

    export_pam_module!(TestModule);
}
