//! Thin Rust wrapper around `pam-sys` for the daemon's needs.
//!
//! Exposes a `Transaction` value that owns the PAM handle and the boxed
//! conversation state, with methods that match the steps of a login flow:
//! `start`, `set_tty`, `authenticate`, `setcred_establish`, `open_session`,
//! `getenvlist`. `Drop` calls `pam_end`.
//!
//! The conversation behaviour is pluggable via the `ConversationHandler`
//! trait: `PresetPassword` for the CLI / spawn paths, an IPC-bridge handler
//! for the daemon's IPC server.
//!
//! After `fork`, both processes have independent copies of the handle (COW).
//! The child calls `setcred_establish` + `open_session` on its copy so that
//! logind associates the session with the child PID. The parent never calls
//! `open_session`, so its `Drop` only needs `pam_end` (no `close_session`).

use anyhow::{ bail, Result };
use pam_sys::raw;
use pam_sys::types::
{
	PamConversation, PamFlag, PamHandle, PamItemType, PamMessage,
	PamMessageStyle, PamResponse, PamReturnCode,
};
use pam_sys::wrapped::{ acct_mgmt, authenticate, end, open_session, setcred, start };
use std::ffi::{ CStr, CString };
use std::os::raw::{ c_char, c_int, c_void };
use std::ptr;

/// Strategy for answering PAM prompts during a transaction.
///
/// `Send` is required because the daemon may run the PAM transaction on a
/// blocking thread (tokio `spawn_blocking`) while the IPC pump stays on the
/// async runtime — the handler crosses that boundary.
pub trait ConversationHandler: Send
{
	/// Respond to a no-echo prompt (password style). Return `None` to refuse.
	fn prompt_blind( &mut self, prompt: &str ) -> Option<String>;

	/// Respond to an echo'd prompt (typically username). Return `None` to refuse.
	fn prompt_echo( &mut self, prompt: &str ) -> Option<String>;

	/// Informational message from PAM. Default: drop on the floor.
	fn info( &mut self, _msg: &str ) {}

	/// Error message from PAM. Default: log via tracing.
	fn error( &mut self, msg: &str ) { tracing::warn!( "PAM error: {msg}" ); }
}

/// Handler that hands back a single fixed password for every blind prompt
/// and ignores echo prompts. Used by the CLI `auth` / `spawn` subcommands
/// and by any flow where the password is already known up front.
pub struct PresetPassword( pub String );

impl ConversationHandler for PresetPassword
{
	fn prompt_blind( &mut self, _: &str ) -> Option<String> { Some( self.0.clone() ) }
	fn prompt_echo( &mut self, _: &str )  -> Option<String> { None }
}

struct ConvState
{
	handler: Box<dyn ConversationHandler>,
}

pub struct Transaction
{
	handle: *mut PamHandle,
	state:  *mut ConvState,
}

impl Transaction
{
	pub fn start( service: &str, user: &str, handler: Box<dyn ConversationHandler> ) -> Result<Self>
	{
		let state = Box::new( ConvState { handler } );
		let state_ptr = Box::into_raw( state );

		let conv = PamConversation
		{
			conv:     Some( converse ),
			data_ptr: state_ptr as *mut c_void,
		};

		let mut handle: *mut PamHandle = ptr::null_mut();
		let rc = start( service, Some( user ), &conv, &mut handle );
		if rc != PamReturnCode::SUCCESS
		{
			unsafe { drop( Box::from_raw( state_ptr ) ); }
			bail!( "pam_start( {service} ): {rc:?}" );
		}

		Ok( Transaction { handle, state: state_ptr } )
	}

	pub fn set_tty( &mut self, tty: &str ) -> Result<()>
	{
		let c = CString::new( tty )?;
		let rc: PamReturnCode = unsafe {
			raw::pam_set_item( self.handle, PamItemType::TTY as c_int, c.as_ptr() as *const c_void ).into()
		};
		if rc != PamReturnCode::SUCCESS
		{
			bail!( "pam_set_item( PAM_TTY ): {rc:?}" );
		}
		Ok( () )
	}

	pub fn putenv( &mut self, name_value: &str ) -> Result<()>
	{
		let h = unsafe { &mut *self.handle };
		let rc = pam_sys::wrapped::putenv( h, name_value );
		if rc != PamReturnCode::SUCCESS { bail!( "pam_putenv( {name_value} ): {rc:?}" ); }
		Ok( () )
	}

	pub fn authenticate( &mut self ) -> Result<()>
	{
		let h = unsafe { &mut *self.handle };
		let rc = authenticate( h, PamFlag::NONE );
		if rc != PamReturnCode::SUCCESS { bail!( "pam_authenticate: {rc:?}" ); }
		let rc = acct_mgmt( h, PamFlag::NONE );
		if rc != PamReturnCode::SUCCESS { bail!( "pam_acct_mgmt: {rc:?}" ); }
		Ok( () )
	}

	pub fn setcred_establish( &mut self ) -> Result<()>
	{
		let h = unsafe { &mut *self.handle };
		let rc = setcred( h, PamFlag::ESTABLISH_CRED );
		if rc != PamReturnCode::SUCCESS { bail!( "pam_setcred( ESTABLISH ): {rc:?}" ); }
		Ok( () )
	}

	pub fn open_session( &mut self ) -> Result<()>
	{
		let h = unsafe { &mut *self.handle };
		let rc = open_session( h, PamFlag::NONE );
		if rc != PamReturnCode::SUCCESS { bail!( "pam_open_session: {rc:?}" ); }
		Ok( () )
	}

	/// Snapshot the PAM environment as `KEY=VALUE` C strings, freeing the
	/// malloc'd buffer returned by libpam. Returns an empty vec if PAM has
	/// no env or allocation failed.
	pub fn getenvlist( &mut self ) -> Vec<CString>
	{
		let list = unsafe { raw::pam_getenvlist( self.handle ) };
		if list.is_null() { return Vec::new(); }
		let mut out = Vec::new();
		unsafe
		{
			let mut i: isize = 0;
			loop
			{
				let entry = *list.offset( i );
				if entry.is_null() { break; }
				out.push( CStr::from_ptr( entry ).to_owned() );
				libc::free( entry as *mut c_void );
				i += 1;
			}
			libc::free( list as *mut c_void );
		}
		out
	}
}

impl Drop for Transaction
{
	fn drop( &mut self )
	{
		unsafe
		{
			if !self.handle.is_null()
			{
				end( &mut *self.handle, PamReturnCode::SUCCESS );
			}
			if !self.state.is_null()
			{
				drop( Box::from_raw( self.state ) );
			}
		}
	}
}

extern "C" fn converse(
	num_msg:     c_int,
	msg:         *mut *mut PamMessage,
	resp:        *mut *mut PamResponse,
	appdata_ptr: *mut c_void,
) -> c_int
{
	unsafe
	{
		if num_msg <= 0 || msg.is_null() || resp.is_null() || appdata_ptr.is_null()
		{
			return PamReturnCode::CONV_ERR as c_int;
		}

		let state = &mut *( appdata_ptr as *mut ConvState );
		let n     = num_msg as usize;

		let responses = libc::calloc( n, std::mem::size_of::<PamResponse>() ) as *mut PamResponse;
		if responses.is_null()
		{
			return PamReturnCode::BUF_ERR as c_int;
		}

		for i in 0..n
		{
			let m = &**msg.add( i );
			let r = &mut *responses.add( i );
			r.resp_retcode = 0;
			r.resp = ptr::null_mut();

			let prompt = if m.msg.is_null()
			{
				String::new()
			} else {
				CStr::from_ptr( m.msg ).to_string_lossy().into_owned()
			};

			let echo_off  = PamMessageStyle::PROMPT_ECHO_OFF as c_int;
			let echo_on   = PamMessageStyle::PROMPT_ECHO_ON  as c_int;
			let error_msg = PamMessageStyle::ERROR_MSG       as c_int;
			let text_info = PamMessageStyle::TEXT_INFO       as c_int;

			let reply = if m.msg_style == echo_off
			{
				state.handler.prompt_blind( &prompt )
			} else if m.msg_style == echo_on {
				state.handler.prompt_echo( &prompt )
			} else if m.msg_style == error_msg {
				state.handler.error( &prompt );
				None
			} else if m.msg_style == text_info {
				state.handler.info( &prompt );
				None
			} else {
				None
			};

			if let Some( reply ) = reply
			{
				let reply_c = match CString::new( reply )
				{
					Ok( v )  => v,
					Err( _ ) => continue,
				};
				let bytes = reply_c.as_bytes_with_nul();
				let buf   = libc::malloc( bytes.len() ) as *mut c_char;
				if buf.is_null()
				{
					for j in 0..=i
					{
						let prior = &mut *responses.add( j );
						if !prior.resp.is_null()
						{
							libc::free( prior.resp as *mut c_void );
						}
					}
					libc::free( responses as *mut c_void );
					return PamReturnCode::BUF_ERR as c_int;
				}
				ptr::copy_nonoverlapping( bytes.as_ptr() as *const c_char, buf, bytes.len() );
				r.resp = buf;
			}
		}

		*resp = responses;
		PamReturnCode::SUCCESS as c_int
	}
}
