//! Minimal PAM client for the fallback unlock surface.
//!
//! Hand-rolled FFI against libpam rather than a wrapper crate: the daemon
//! asks exactly one question ("does this PIN authenticate this user") and a
//! dependency is a bigger liability than 100 lines of well-understood
//! unsafe. pam_unix answers via the setuid unix_chkpwd helper, so this works
//! from an unprivileged session process — the same mechanism hyprlock and
//! the shell's PamContext rely on.
//!
//! Doctrine: auth goes through PAM, always (SESSION-AUTHORITY-DOCTRINE §4).
//! This module never inspects credentials itself; it carries the PIN to the
//! stack configured for the service and reports the verdict.
//!
//! The expected PAM service config is documented in
//! docs/pam-souveraine-sessiond.md. The file `/etc/pam.d/souveraine-sessiond`
//! is shipped as root-owned system config by the OS overlay, never by this
//! crate. Without it, pam_start fails and the lock session does not start.

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

const PAM_SUCCESS: c_int = 0;
const PAM_CONV_ERR: c_int = 19;

const PAM_PROMPT_ECHO_OFF: c_int = 1;
const PAM_PROMPT_ECHO_ON: c_int = 2;
const PAM_ERROR_MSG: c_int = 3;
const PAM_TEXT_INFO: c_int = 4;

#[repr(C)]
struct PamMessage {
    msg_style: c_int,
    msg: *const c_char,
}

#[repr(C)]
struct PamResponse {
    resp: *mut c_char,
    resp_retcode: c_int,
}

#[repr(C)]
struct PamConv {
    conv: unsafe extern "C" fn(
        num_msg: c_int,
        msg: *mut *const PamMessage,
        resp: *mut *mut PamResponse,
        appdata_ptr: *mut c_void,
    ) -> c_int,
    appdata_ptr: *mut c_void,
}

#[link(name = "pam")]
extern "C" {
    fn pam_start(
        service: *const c_char,
        user: *const c_char,
        conv: *const PamConv,
        handle: *mut *mut c_void,
    ) -> c_int;
    fn pam_authenticate(handle: *mut c_void, flags: c_int) -> c_int;
    fn pam_end(handle: *mut c_void, status: c_int) -> c_int;
    fn pam_strerror(handle: *mut c_void, errnum: c_int) -> *const c_char;
}

/// The conversation only ever answers prompts with the PIN. Info/error
/// messages are acknowledged and dropped — the fallback surface has nowhere
/// to render a module's prose and must not block on it.
unsafe extern "C" fn conv_fn(
    num_msg: c_int,
    msg: *mut *const PamMessage,
    resp: *mut *mut PamResponse,
    appdata_ptr: *mut c_void,
) -> c_int {
    if num_msg <= 0 || msg.is_null() || resp.is_null() || appdata_ptr.is_null() {
        return PAM_CONV_ERR;
    }
    let pin = &*(appdata_ptr as *const CString);

    // Linux-PAM passes an array of message pointers. Responses are freed by
    // the module with free(), so they must come from malloc/strdup.
    let responses =
        libc::calloc(num_msg as usize, std::mem::size_of::<PamResponse>()) as *mut PamResponse;
    if responses.is_null() {
        return PAM_CONV_ERR;
    }

    // Linux-PAM convention: *msg points at a contiguous array of messages
    // (modules there prompt one message at a time anyway).
    for i in 0..num_msg as usize {
        let m = (*msg).add(i);
        if m.is_null() {
            libc::free(responses as *mut c_void);
            return PAM_CONV_ERR;
        }
        let slot = &mut *responses.add(i);
        match (*m).msg_style {
            PAM_PROMPT_ECHO_OFF | PAM_PROMPT_ECHO_ON => {
                slot.resp = libc::strdup(pin.as_ptr());
                if slot.resp.is_null() {
                    libc::free(responses as *mut c_void);
                    return PAM_CONV_ERR;
                }
            }
            PAM_ERROR_MSG | PAM_TEXT_INFO => {
                slot.resp = std::ptr::null_mut();
            }
            _ => {
                libc::free(responses as *mut c_void);
                return PAM_CONV_ERR;
            }
        }
        slot.resp_retcode = 0;
    }

    *resp = responses;
    PAM_SUCCESS
}

/// Run one authenticate conversation. Blocking (pam_unix sleeps ~2s on a
/// wrong answer); callers own threading. Returns Ok(()) on success and the
/// stack's complaint otherwise.
pub fn authenticate(service: &str, user: &str, pin: &str) -> Result<(), String> {
    let service = CString::new(service).map_err(|_| "service contains NUL".to_string())?;
    let user_c = CString::new(user).map_err(|_| "user contains NUL".to_string())?;
    let pin = CString::new(pin).map_err(|_| "pin contains NUL".to_string())?;

    // Boxed so the conversation callback has a stable pointer for the whole
    // transaction; dropped after pam_end.
    let pin = Box::new(pin);
    let conv = PamConv {
        conv: conv_fn,
        appdata_ptr: &*pin as *const CString as *mut c_void,
    };

    let mut handle: *mut c_void = std::ptr::null_mut();
    // SAFETY: all pointers live across the calls; handle is only used
    // between a successful pam_start and pam_end.
    unsafe {
        let rc = pam_start(service.as_ptr(), user_c.as_ptr(), &conv, &mut handle);
        if rc != PAM_SUCCESS {
            return Err(format!("pam_start failed (code {rc})"));
        }
        let rc = pam_authenticate(handle, 0);
        let verdict = if rc == PAM_SUCCESS {
            Ok(())
        } else {
            let msg = pam_strerror(handle, rc);
            Err(if msg.is_null() {
                format!("authentication failed (code {rc})")
            } else {
                CStr::from_ptr(msg).to_string_lossy().into_owned()
            })
        };
        pam_end(handle, rc);
        verdict
    }
}

/// The user this session belongs to, from the process's own identity —
/// sessiond authenticates the seat owner, never a name a caller supplies.
pub fn session_user() -> Result<String, String> {
    // SAFETY: getpwuid_r with a stack buffer; result pointer checked.
    unsafe {
        let uid = libc::getuid();
        let mut pwd: libc::passwd = std::mem::zeroed();
        let mut buf = [0u8; 1024];
        let mut result: *mut libc::passwd = std::ptr::null_mut();
        // libc's `c_char` is i8 on some targets and u8 on others (aarch64),
        // so the buffer is `u8` and we cast through `c_char` to satisfy
        // whichever signature the target's libc crate exposes.
        let rc = libc::getpwuid_r(
            uid,
            &mut pwd,
            buf.as_mut_ptr() as *mut libc::c_char,
            buf.len(),
            &mut result,
        );
        if rc != 0 || result.is_null() {
            return Err(format!("getpwuid_r failed for uid {uid}"));
        }
        Ok(CStr::from_ptr(pwd.pw_name).to_string_lossy().into_owned())
    }
}
