//! Authentication related structure and functions
use std::os::raw::{c_char, c_void};
use std::sync::Mutex;
use std::{env, ffi::CStr};

use crate::{conv, enums::*, functions::*, types::*};

/// Global lock protecting concurrent access to the process environment.
///
/// `std::env::set_var` / `std::env::remove_var` are MT-unsafe by the POSIX
/// standard (mutate global state without synchronization) and are marked
/// `unsafe` in Rust 2024 edition. This mutex serializes all environment
/// mutations from this crate to prevent data races when multiple `Client`
/// instances run on different threads.
///
/// External code (C libraries, other Rust crates) that also mutates the
/// environment bypasses this lock — the caller is responsible for ensuring
/// no concurrent environ access from outside this crate.
/// See M1 (race condition audit finding).
static ENV_LOCK: Mutex<()> = Mutex::new(());

/// Check whether a character is unsafe in an environment variable value.
///
/// Filters Unicode control (Cc), format (Cf), and line/paragraph separator
/// (Zl/Zp) characters that could cause:
/// - Line-break injection in log parsers (U+2028, U+2029)
/// - Visual spoofing via bidi overrides (U+202A–U+202E, U+2066–U+2069)
///   — the classic Trojan Source attack vector (CVE-2021-42574)
/// - Hidden text via zero-width characters (U+200B–U+200F)
/// - Encoding artifacts (U+FEFF BOM)
///
/// See MED-07 and M3 (residual gap).
fn env_char_is_risky(c: char) -> bool {
    c.is_control() // Cc: \n, \r, \0, \x1B, etc.
        // Zl/Zp: line/paragraph separators (break log parsers)
        || matches!(c, '\u{2028}' | '\u{2029}')
        // Cf: zero-width space and directional marks
        || matches!(c, '\u{200B}'..='\u{200F}')
        // Cf: bidi embedding/override (Trojan Source CVE-2021-42574)
        || matches!(c, '\u{202A}'..='\u{202E}')
        // Cf: bidi isolates
        || matches!(c, '\u{2066}'..='\u{2069}')
        // Cf: BOM
        || c == '\u{FEFF}'
}

/// Main struct to authenticate a user
///
/// You need to create an instance of it to start an authentication process. If you
/// want a simple password-based authentication, you can use `Client::with_password`,
/// and to the following flow:
///
/// ```no_run
/// use pam_unix::Client;
///
/// let mut client = Client::with_password("system-auth")
///         .expect("Failed to init PAM client.");
/// // Preset the login & password we will use for authentication
/// client.conversation_mut().set_credentials("login", "password");
/// // Actually try to authenticate:
/// client.authenticate().expect("Authentication failed!");
/// // Now that we are authenticated, it's possible to open a sesssion:
/// client.open_session().expect("Failed to open a session!");
/// ```
///
/// If you wish to customise the PAM conversation function, you should rather create your
/// client with `Client::with_handler`, providing a struct implementing the
/// `conv::Conversation` trait. You can then mutably access your conversation handler using the
/// `Client::handler_mut` method.
///
/// By default, the `Client` will close any opened session when dropped. If you don't
/// want this, you can call `set_close_on_drop(false)`.
pub struct Client<C: conv::Conversation> {
    /// Whether the Client should close the session on drop.
    /// Made private to prevent accidental modification (LOW-01).
    /// Use `set_close_on_drop()` to change.
    close_on_drop: bool,
    handle: PamContext,
    conversation: Box<C>,
    is_authenticated: bool,
    has_open_session: bool,
    last_code: PamReturnCode,
}

impl Client<conv::PasswordConv> {
    /// Create a new `Client` with the given service name and a password-based conversation
    pub fn with_password(service: &str) -> PamResult<Client<conv::PasswordConv>> {
        Client::with_conversation(service, conv::PasswordConv::new())
    }

    /// Create a `Client` using an alternative PAM configuration directory
    /// instead of `/etc/pam.d`. Linux only (pam_start_confdir is a Linux-PAM
    /// extension). Useful for isolated integration testing and sandboxing.
    #[cfg(target_os = "linux")]
    pub fn with_password_confdir(
        service: &str,
        confdir: &str,
    ) -> PamResult<Client<conv::PasswordConv>> {
        Client::with_conversation_confdir(service, conv::PasswordConv::new(), confdir)
    }
}

impl<C: conv::Conversation> Client<C> {
    /// Create a new `Client` with the given service name and conversation handler
    pub fn with_conversation(service: &str, conversation: C) -> PamResult<Client<C>> {
        let mut conversation = Box::new(conversation);
        let conv = conv::into_pam_conv(&mut *conversation);

        let handle = start(service, None, &conv)?;
        Ok(Client {
            close_on_drop: true,
            handle,
            conversation,
            is_authenticated: false,
            has_open_session: false,
            last_code: PamReturnCode::Success,
        })
    }

    /// Create a `Client` using an alternative PAM configuration directory.
    /// Linux only. See `start_confdir()` for details.
    #[cfg(target_os = "linux")]
    pub fn with_conversation_confdir(
        service: &str,
        conversation: C,
        confdir: &str,
    ) -> PamResult<Client<C>> {
        let mut conversation = Box::new(conversation);
        let conv = conv::into_pam_conv(&mut *conversation);

        let handle = start_confdir(service, None, &conv, confdir)?;
        Ok(Client {
            close_on_drop: true,
            handle,
            conversation,
            is_authenticated: false,
            has_open_session: false,
            last_code: PamReturnCode::Success,
        })
    }

    /// Immutable access to the conversation handler of this Client
    pub fn conversation(&self) -> &C {
        &*self.conversation
    }

    /// Mutable access to the conversation handler of this Client
    pub fn conversation_mut(&mut self) -> &mut C {
        &mut *self.conversation
    }

    /// Control whether the session is automatically closed on drop.
    /// Default is `true`. Set to `false` if you need the session to
    /// outlive this Client instance (LOW-01).
    pub fn set_close_on_drop(&mut self, value: bool) {
        self.close_on_drop = value;
    }

    /// Set a delay for failed authentication attempts (brute-force mitigation).
    ///
    /// PAM will insert 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.
    ///
    /// Call this before `authenticate()` to set a baseline delay, or after
    /// each failed attempt to implement exponential backoff. The delay is
    /// reset when the PAM transaction ends.
    ///
    /// # Example: 2-second base delay
    /// ```
    /// # use pam_unix::Client;
    /// # let mut client = Client::with_password("login").unwrap();
    /// client.set_fail_delay(2_000_000); // 2 seconds in microseconds
    /// ```
    pub fn set_fail_delay(&mut self, usec: u32) -> PamResult<()> {
        let code = fail_delay(self.handle.as_mut(), usec);
        if code != PamReturnCode::Success {
            return Err(code.into());
        }
        Ok(())
    }

    /// Set the remote host (PAM_RHOST) for this transaction.
    ///
    /// Required by pam_access, pam_faillock, and audit modules.
    /// Should be called before `authenticate()`.
    pub fn set_rhost(&mut self, rhost: &str) -> PamResult<()> {
        set_rhost(self.handle.as_mut(), rhost)
    }

    /// Set the terminal name (PAM_TTY) for this transaction.
    pub fn set_tty(&mut self, tty: &str) -> PamResult<()> {
        set_tty(self.handle.as_mut(), tty)
    }

    /// Set the requesting user (PAM_RUSER) for this transaction.
    pub fn set_ruser(&mut self, ruser: &str) -> PamResult<()> {
        set_ruser(self.handle.as_mut(), ruser)
    }

    /// Perform authentication with the provided credentials
    pub fn authenticate(&mut self) -> PamResult<()> {
        self.last_code = authenticate(self.handle.as_mut(), PamFlags::NONE);
        if self.last_code != PamReturnCode::Success {
            return Err(From::from(self.last_code));
        }

        self.is_authenticated = true;

        self.last_code = acct_mgmt(self.handle.as_mut(), PamFlags::NONE);
        if self.last_code != PamReturnCode::Success {
            return self.reset();
        }
        Ok(())
    }

    /// Change the authentication token (password) for the current user.
    ///
    /// Unlike `open_session()`, this method does **not** require prior
    /// authentication (`is_authenticated == true`). This is intentional:
    /// `pam_chauthtok` is legitimately called without authentication in
    /// administrative scenarios — for example, `root` using the `passwd`
    /// utility to change another user's password. In those cases the PAM
    /// password stack performs its own re-authentication as configured
    /// in `/etc/pam.d/`. Adding an unconditional check here would break
    /// these legitimate use cases.
    ///
    /// Valid flags: `SILENT`, `CHANGE_EXPIRED_AUTHTOK`, or combinations via `|`.
    pub fn change_authentication_token(&mut self, flags: PamFlags) -> PamResult<()> {
        self.last_code = chauthtok(self.handle.as_mut(), flags);
        if self.last_code != PamReturnCode::Success {
            return Err(From::from(self.last_code));
        }
        Ok(())
    }

    /// Perform the get_item / PAM_USER to retrive the username
    pub fn get_user(&mut self) -> PamResult<String> {
        get_item(self.handle.as_ref(), PamItemType::User).and_then(|result| {
            // PAM_USER is always a char*; use pointer cast instead of transmute.
            // transmute silently reinterprets bits and strips lifetime info (HIGH-03).
            let ptr: *const c_char = result as *const c_void as *const c_char;
            if ptr.is_null() {
                return Err(PamError(PamReturnCode::System_Err));
            }
            let username = unsafe { CStr::from_ptr(ptr) };
            username
                .to_str()
                .map(|s| s.to_string())
                .map_err(|_| PamError(PamReturnCode::System_Err))
        })
    }

    /// Open a session for a previously authenticated user and
    /// initialize the environment appropriately (in PAM and regular enviroment variables).
    pub fn open_session(&mut self) -> PamResult<()> {
        if !self.is_authenticated {
            return Err(PamReturnCode::Perm_Denied.into());
        }

        self.last_code = setcred(self.handle.as_mut(), PamFlags::ESTABLISH_CRED);
        if self.last_code != PamReturnCode::Success {
            return self.reset();
        }

        self.last_code = open_session(self.handle.as_mut(), false);
        if self.last_code != PamReturnCode::Success {
            return self.reset();
        }

        // Follow openSSH and call pam_setcred before and after open_session.
        // If Reinitialize_Cred fails, the session is already open — we must
        // close it explicitly before resetting. See HIGH-03 (session leak).
        self.last_code = setcred(self.handle.as_mut(), PamFlags::REINITIALIZE_CRED);
        if self.last_code != PamReturnCode::Success {
            close_session(self.handle.as_mut(), false);
            return self.reset();
        }

        self.has_open_session = true;
        self.initialize_environment()
    }

    // Initialize the client environment with common variables.
    // Currently always called from Client.open_session()
    fn initialize_environment(&mut self) -> PamResult<()> {
        use uzers::os::unix::UserExt;

        let username = self.get_user()?;
        let user =
            uzers::get_user_by_name(&username).ok_or(PamError(PamReturnCode::User_Unknown))?;

        // Sanitize values from user database (/etc/passwd, NSS, LDAP).
        // A compromised NSS backend or crafted passwd entry could inject
        // control characters (newlines, nulls) into home_dir or shell.
        // While pam_putenv only splits on the first '=', control characters
        // in the value could cause undefined behavior in shells or programs
        // that later read the environment. See MED-07.
        let user_name = Self::sanitize_env_value(&user.name().to_string_lossy());
        let home = Self::sanitize_env_value(&user.home_dir().to_string_lossy());
        let shell = Self::sanitize_env_value(&user.shell().to_string_lossy());

        self.set_env("USER", &user_name)?;
        self.set_env("LOGNAME", &user_name)?;
        self.set_env("HOME", &home)?;
        self.set_env("PWD", &home)?;
        self.set_env("SHELL", &shell)?;
        Ok(())
    }

    /// Remove control and formatting characters from environment values.
    ///
    /// See `env_char_is_risky()` for the full list of filtered characters
    /// (Cc, Cf, Zl/Zp categories). All filtered characters are replaced
    /// with spaces. See MED-07 and M3 (residual gap).
    fn sanitize_env_value(raw: &str) -> String {
        raw.chars()
            .map(|c| if env_char_is_risky(c) { ' ' } else { c })
            .collect()
    }

    /// Set an environment variable in both the PAM environment and the process.
    ///
    /// Protects the `set_var` call with a global `ENV_LOCK` mutex to prevent
    /// data races when multiple `Client` instances mutate the environment from
    /// different threads. External code that mutates `environ` outside of this
    /// crate bypasses the lock — the caller is responsible for ensuring no
    /// concurrent access. See M1 (race condition audit finding).
    ///
    /// # Safety
    /// While the lock serializes mutations from this crate, `set_var` is
    /// fundamentally MT-unsafe per POSIX and will be marked `unsafe` in
    /// Rust 2024. The caller must guarantee no other thread (Rust or C) reads
    /// or writes the environment concurrently.
    fn set_env(&mut self, key: &str, value: &str) -> PamResult<()> {
        let _guard = ENV_LOCK.lock().unwrap();
        // SAFETY: protected by ENV_LOCK against concurrent calls from this
        // crate. External mutations of the environ pointer are outside our
        // control — documented as a caller responsibility.
        unsafe {
            env::set_var(key, value);
        }

        let name_value = format!("{}={}", key, value);
        putenv(self.handle.as_mut(), &name_value)
    }

    fn reset(&mut self) -> PamResult<()> {
        let cred_code = setcred(self.handle.as_mut(), PamFlags::DELETE_CRED);
        if cred_code != PamReturnCode::Success {
            eprintln!(
                "[PAM] Warning: setcred(Delete_Cred) failed during reset: {}",
                cred_code
            );
        }
        self.is_authenticated = false;
        Err(From::from(self.last_code))
    }
}

impl<C: conv::Conversation> Drop for Client<C> {
    fn drop(&mut self) {
        if self.has_open_session && self.close_on_drop {
            let cs_code = close_session(self.handle.as_mut(), false);
            if cs_code != PamReturnCode::Success {
                eprintln!(
                    "[PAM] Warning: close_session failed during drop: {}",
                    cs_code
                );
            }
        }
        // Delete credentials before ending the transaction. The status passed
        // to pam_end must reflect the actual transaction outcome (auth/session),
        // not the cleanup operation. PAM modules use this status for audit/log
        // callbacks registered via pam_set_data. See B1.
        setcred(self.handle.as_mut(), PamFlags::DELETE_CRED);
        end(self.handle.as_mut(), self.last_code);
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn sanitize_env_value_clean() {
        assert_eq!(
            Client::<conv::PasswordConv>::sanitize_env_value("/usr/bin/bash"),
            "/usr/bin/bash"
        );
    }

    #[test]
    fn sanitize_env_value_removes_newline() {
        assert_eq!(
            Client::<conv::PasswordConv>::sanitize_env_value("/bin/sh\nrm -rf /"),
            "/bin/sh rm -rf /"
        );
    }

    #[test]
    fn sanitize_env_value_removes_null() {
        assert_eq!(
            Client::<conv::PasswordConv>::sanitize_env_value("/tmp/\0evil"),
            "/tmp/ evil"
        );
    }

    #[test]
    fn sanitize_env_value_removes_carriage_return() {
        assert_eq!(
            Client::<conv::PasswordConv>::sanitize_env_value("/bin/bash\r"),
            "/bin/bash "
        );
    }

    #[test]
    fn sanitize_env_value_preserves_unicode() {
        assert_eq!(
            Client::<conv::PasswordConv>::sanitize_env_value("/home/café"),
            "/home/café"
        );
    }

    #[test]
    fn sanitize_env_value_empty_string() {
        assert_eq!(Client::<conv::PasswordConv>::sanitize_env_value(""), "");
    }

    // --- env_char_is_risky (M3: Unicode formatting chars) ---

    #[test]
    fn env_char_risky_control_chars() {
        assert!(env_char_is_risky('\n'));
        assert!(env_char_is_risky('\r'));
        assert!(env_char_is_risky('\0'));
        assert!(env_char_is_risky('\x1B')); // ESC
    }

    #[test]
    fn env_char_risky_safe_printable() {
        assert!(!env_char_is_risky('a'));
        assert!(!env_char_is_risky('/'));
        assert!(!env_char_is_risky('é')); // Unicode letter
        assert!(!env_char_is_risky(' ')); // regular space is safe
    }

    #[test]
    fn env_char_risky_line_separator() {
        assert!(env_char_is_risky('\u{2028}')); // LINE SEPARATOR
    }

    #[test]
    fn env_char_risky_paragraph_separator() {
        assert!(env_char_is_risky('\u{2029}')); // PARAGRAPH SEPARATOR
    }

    #[test]
    fn env_char_risky_bidi_override() {
        // U+202E RIGHT-TO-LEFT OVERRIDE — Trojan Source attack vector
        assert!(env_char_is_risky('\u{202E}'));
        // Other bidi controls in range U+202A–U+202E
        assert!(env_char_is_risky('\u{202A}')); // LRE
        assert!(env_char_is_risky('\u{202D}')); // LRO
    }

    #[test]
    fn env_char_risky_bidi_isolates() {
        assert!(env_char_is_risky('\u{2066}')); // LRI
        assert!(env_char_is_risky('\u{2069}')); // PDI
    }

    #[test]
    fn env_char_risky_zero_width() {
        assert!(env_char_is_risky('\u{200B}')); // ZERO WIDTH SPACE
        assert!(env_char_is_risky('\u{200F}')); // RIGHT-TO-LEFT MARK
    }

    #[test]
    fn env_char_risky_bom() {
        assert!(env_char_is_risky('\u{FEFF}')); // BOM
    }

    #[test]
    fn sanitize_env_value_strips_bidi_override() {
        let input = "/bin/sh\u{202E}rm -rf /";
        let output = Client::<conv::PasswordConv>::sanitize_env_value(input);
        assert!(
            !output.contains('\u{202E}'),
            "Bidi override must be removed"
        );
    }
}
