use libc::c_char;
use memchr::memchr;

use std::ffi::{CStr, OsString};
use std::vec::IntoIter;

pub struct PamEnvList {
    inner: IntoIter<(OsString, OsString)>,
}

impl Iterator for PamEnvList {
    type Item = (String, String);

    fn next(&mut self) -> Option<(String, String)> {
        self.inner.next().map(|(a, b)| {
            // PAM environment values may contain non-UTF-8 bytes; use lossy conversion
            // instead of unwrap() to avoid panicking on malformed data (MED-01).
            (
                a.into_string()
                    .unwrap_or_else(|e| e.to_string_lossy().into_owned()),
                b.into_string()
                    .unwrap_or_else(|e| e.to_string_lossy().into_owned()),
            )
        })
    }
    fn size_hint(&self) -> (usize, Option<usize>) {
        self.inner.size_hint()
    }
}

impl PamEnvList {
    pub(crate) fn from_ptr(ptr: *const *const c_char) -> PamEnvList {
        let mut result = Vec::new();

        unsafe {
            let mut current = ptr;
            if !current.is_null() {
                while !(*current).is_null() {
                    if let Some(key_value) = parse_env_line(CStr::from_ptr(*current).to_bytes()) {
                        result.push(key_value);
                    }
                    current = current.add(1);
                }
            }
        }

        drop_env_list(ptr);
        return PamEnvList {
            inner: result.into_iter(),
        };
    }
}

fn parse_env_line(input: &[u8]) -> Option<(OsString, OsString)> {
    // Strategy (copied from glibc): Variable name and value are separated
    // by an ASCII equals sign '='. Since a variable name must not be
    // empty, allow variable names starting with an equals sign. Skip all
    // malformed lines.
    use std::os::unix::prelude::OsStringExt;

    if input.is_empty() {
        return None;
    }
    let pos = memchr(b'=', input)?;
    Some((
        OsStringExt::from_vec(input[..pos].to_vec()), // key: before '='
        OsStringExt::from_vec(input[pos + 1..].to_vec()), // value: after '='
    ))
}

/// Platforms with Linux-PAM or OpenPAM (FreeBSD, NetBSD, DragonFly BSD, macOS)
/// that provide `pam_misc_drop_env` in `<security/pam_misc.h>`.
#[cfg(any(
    target_os = "linux",
    target_os = "freebsd",
    target_os = "netbsd",
    target_os = "macos",
    target_os = "dragonfly"
))]
fn drop_env_list(ptr: *const *const c_char) {
    // Linux-PAM and OpenPAM both provide this function to free the env list
    unsafe { crate::ffi::pam_misc_drop_env(ptr as *mut *mut c_char) };
}

/// Fallback for platforms where `pam_misc_drop_env` is unavailable.
/// pam_getenvlist returns a malloc'd null-terminated array of strdup'd strings
/// on all PAM implementations; manual cleanup mirrors what pam_misc_drop_env does.
///
/// Previously this function was completely broken: it dereferenced *ptr before
/// the null check, called free() on non-heap pointers, had an infinite loop,
/// and a double-free. See security audit CRIT-02.
#[cfg(not(any(
    target_os = "linux",
    target_os = "freebsd",
    target_os = "netbsd",
    target_os = "macos",
    target_os = "dragonfly"
)))]
fn drop_env_list(ptr: *const *const c_char) {
    if ptr.is_null() {
        return;
    }

    // Free each individual environment string, then the array itself
    unsafe {
        let mut i = 0usize;
        loop {
            let entry = *ptr.add(i);
            if entry.is_null() {
                break;
            }
            libc::free(entry as *mut libc::c_void);
            i += 1;
        }
        libc::free(ptr as *mut libc::c_void);
    }
}

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

    #[test]
    fn parse_env_line_normal() {
        let result = parse_env_line(b"FOO=BAR");
        assert!(result.is_some());
        let (key, value) = result.unwrap();
        assert_eq!(key, OsString::from("FOO"));
        assert_eq!(value, OsString::from("BAR"));
    }

    #[test]
    fn parse_env_line_value_with_equals() {
        // VALUE contains '=' — only first '=' is the separator
        let result = parse_env_line(b"HOME=/usr/local/bin");
        assert!(result.is_some());
        let (key, value) = result.unwrap();
        assert_eq!(key, OsString::from("HOME"));
        assert_eq!(value, OsString::from("/usr/local/bin"));
    }

    #[test]
    fn parse_env_line_empty_input() {
        assert!(parse_env_line(b"").is_none());
    }

    #[test]
    fn parse_env_line_no_equals() {
        // No separator → skip malformed line
        assert!(parse_env_line(b"FOOBAR").is_none());
    }

    #[test]
    fn parse_env_line_starts_with_equals() {
        // Glibc allows variable names starting with '='
        let result = parse_env_line(b"=unusual_key=value");
        assert!(result.is_some());
        let (key, value) = result.unwrap();
        // First '=' at position 0, so key is empty and value is "unusual_key=value"
        // Note: glibc would treat it differently, but this is the documented behavior
        assert_eq!(key, OsString::from(""));
        assert_eq!(value, OsString::from("unusual_key=value"));
    }

    #[test]
    fn parse_env_line_only_equals() {
        let result = parse_env_line(b"=");
        assert!(result.is_some());
        let (key, value) = result.unwrap();
        assert_eq!(key, OsString::from(""));
        assert_eq!(value, OsString::from(""));
    }

    #[test]
    fn parse_env_line_non_utf8_bytes() {
        use std::os::unix::prelude::OsStringExt;
        // PAM may return arbitrary bytes in environment values
        let input = b"KEY=\xFF\xFE";
        let result = parse_env_line(input);
        assert!(result.is_some());
        let (key, value) = result.unwrap();
        assert_eq!(key, OsString::from("KEY"));
        assert_eq!(value, OsString::from_vec(vec![0xFF, 0xFE]));
    }

    #[test]
    fn pam_env_list_next_converts_lossy() {
        use std::os::unix::prelude::OsStringExt;
        let os_strings = vec![(
            OsString::from("USER"),
            OsStringExt::from_vec(vec![0xC3, 0x28]), // invalid UTF-8
        )];
        let env_list = PamEnvList {
            inner: os_strings.into_iter(),
        };
        let result: Vec<_> = env_list.collect();
        assert_eq!(result.len(), 1);
        assert_eq!(result[0].0, "USER");
        // Invalid UTF-8 should be replaced with replacement character, not panic
        assert!(result[0].1.contains('\u{FFFD}'));
    }
}
