//! Privileged session bring-up: authenticate in the parent, fork, and have
//! the child do `pam_setcred(ESTABLISH)` + `pam_open_session` (so logind
//! sees the child PID as session leader), drop privileges and exec the
//! configured session command on a dedicated VT.

use anyhow::{ bail, Result };
use std::ffi::{ CStr, CString };
use std::path::{ Path, PathBuf };

use crate::pam_session::{ PresetPassword, Transaction };
use crate::vt;

pub struct UserInfo
{
	pub name:  String,
	pub uid:   libc::uid_t,
	pub gid:   libc::gid_t,
	pub home:  PathBuf,
	pub shell: PathBuf,
}

pub fn lookup_user( name: &str ) -> Result<UserInfo>
{
	let c_name = CString::new( name )?;
	let pwd = unsafe { libc::getpwnam( c_name.as_ptr() ) };
	if pwd.is_null()
	{
		bail!( "getpwnam( {name} ): user not found" );
	}
	unsafe
	{
		let p = &*pwd;
		let home  = CStr::from_ptr( p.pw_dir   ).to_string_lossy().into_owned();
		let shell = CStr::from_ptr( p.pw_shell ).to_string_lossy().into_owned();
		let shell = if shell.is_empty() { "/bin/sh".to_string() } else { shell };
		Ok( UserInfo
		{
			name:  name.to_string(),
			uid:   p.pw_uid,
			gid:   p.pw_gid,
			home:  PathBuf::from( home ),
			shell: PathBuf::from( shell ),
		} )
	}
}

/// High-level entry used by the CLI `spawn` subcommand. Allocates a VT,
/// authenticates with a preset password, and forks the session.
pub fn spawn(
	service:  &str,
	username: &str,
	password: &str,
	command:  &Path,
	env:      &[ ( String, String ) ],
) -> Result<i32>
{
	let user      = lookup_user( username )?;
	let target_vt = vt::open_query()?;
	let tty_path  = format!( "/dev/tty{}", target_vt );
	tracing::info!( "session: target VT {target_vt} ({tty_path})" );

	let mut tx = Transaction::start(
		service,
		username,
		Box::new( PresetPassword( password.to_string() ) ),
	)?;
	tx.set_tty( &tty_path )?;
	tx.authenticate()?;
	tracing::info!( "session: authenticated {username}" );

	spawn_post_auth( tx, user, target_vt, command, env )
}

/// CLI-side wrapper: fork, switch VT immediately, wait for the child.
pub fn spawn_post_auth(
	tx:         Transaction,
	user:       UserInfo,
	target_vt:  u32,
	command:    &Path,
	env:        &[ ( String, String ) ],
) -> Result<i32>
{
	let pid = fork_session( tx, user, target_vt, command, &[], env )?;
	vt::activate( target_vt )?;
	vt::wait_active( target_vt )?;
	tracing::info!( "session: switched to VT {target_vt}" );
	wait_session( pid )
}

/// Post-authenticate fork only. Returns the child PID in the parent. The
/// IPC server uses this so it can hold the VT switch until crustace signals
/// "first frame ready" — keeping the greeter visible until the user session
/// is actually about to paint.
pub fn fork_session(
	tx:         Transaction,
	user:       UserInfo,
	target_vt:  u32,
	command:    &Path,
	extra_args: &[ String ],
	env:        &[ ( String, String ) ],
) -> Result<libc::pid_t>
{
	if !command.is_absolute()
	{
		bail!( "session command must be an absolute path, got {:?}", command );
	}

	let tty_path = format!( "/dev/tty{}", target_vt );

	let tty_c     = CString::new( tty_path )?;
	let home_c    = CString::new( user.home.to_string_lossy().into_owned() )?;
	let name_c    = CString::new( user.name.clone() )?;
	let shell_c   = CString::new( user.shell.to_string_lossy().into_owned() )?;
	let command_c = CString::new( command.to_string_lossy().into_owned() )?;

	let basename = command.file_name().and_then( |f| f.to_str() ).unwrap_or( "session" );
	let arg0_str = if is_shell_basename( basename )
	{
		format!( "-{basename}" )
	} else {
		basename.to_string()
	};

	// Build argv = [arg0, extra_args..]. CStrings stay owned here so their
	// pointers remain valid through `execv` (the child borrows them via
	// `argv_ptrs`).
	let mut argv_owned: Vec<CString> = Vec::with_capacity( 1 + extra_args.len() );
	argv_owned.push( CString::new( arg0_str )? );
	for a in extra_args
	{
		argv_owned.push( CString::new( a.as_str() )? );
	}

	let mut env_strings: Vec<CString> = Vec::with_capacity( env.len() );
	for ( k, v ) in env
	{
		env_strings.push( CString::new( k.as_str() )? );
		env_strings.push( CString::new( v.as_str() )? );
	}
	for ( k, v ) in env
	{
		if matches!(
			k.as_str(),
			"LOGINMANAGER_SESSION_SOCK" | "XDG_SESSION_TYPE" | "XDG_CURRENT_DESKTOP" | "XDG_SESSION_DESKTOP" | "WAYLAND_DISPLAY"
		)
		{
			tracing::info!( "session: env {k}={v}" );
		}
	}

	let pid = unsafe { libc::fork() };
	match pid
	{
		-1 => bail!( "fork: {}", std::io::Error::last_os_error() ),
		0  =>
		{
			child_exec(
				tx, &tty_c, &home_c, &name_c, &shell_c, &command_c, &argv_owned,
				&env_strings, user.uid, user.gid, target_vt,
			);
		}
		_ =>
		{
			tracing::info!( "session: forked child pid {pid}" );
			Ok( pid )
		}
	}
}

pub fn wait_session( pid: libc::pid_t ) -> Result<i32>
{
	let mut status: libc::c_int = 0;
	let waited = unsafe { libc::waitpid( pid, &mut status, 0 ) };
	if waited < 0
	{
		tracing::warn!( "waitpid: {}", std::io::Error::last_os_error() );
	}
	tracing::info!( "session: child exited (status raw {status})" );
	Ok( status )
}

fn is_shell_basename( name: &str ) -> bool
{
	matches!(
		name,
		"sh" | "bash" | "zsh" | "fish" | "dash" | "ksh"
	)
}

fn child_exec(
	mut tx:  Transaction,
	tty:     &CString,
	home:    &CString,
	name:    &CString,
	shell:   &CString,
	command: &CString,
	argv:    &[ CString ],
	env:     &[ CString ],
	uid:     libc::uid_t,
	gid:     libc::gid_t,
	vt:      u32,
) -> !
{
	if let Err( e ) = tx.setcred_establish()
	{
		child_fail_str( &format!( "pam_setcred: {e}\n" ) );
	}
	if let Err( e ) = tx.open_session()
	{
		child_fail_str( &format!( "pam_open_session: {e}\n" ) );
	}
	let pam_env = tx.getenvlist();

	unsafe
	{
		if libc::setsid() < 0 { child_fail( b"setsid\n" ); }

		let fd = libc::open( tty.as_ptr(), libc::O_RDWR );
		if fd < 0 { child_fail( b"open(tty)\n" ); }
		if libc::ioctl( fd, libc::TIOCSCTTY, 0 ) < 0 { child_fail( b"TIOCSCTTY\n" ); }

		libc::dup2( fd, 0 );
		if fd > 2 { libc::close( fd ); }

		if libc::setgid( gid )                          < 0 { child_fail( b"setgid\n" ); }
		if libc::initgroups( name.as_ptr(), gid )       < 0 { child_fail( b"initgroups\n" ); }
		if libc::setuid( uid )                          < 0 { child_fail( b"setuid\n" ); }

		if libc::chdir( home.as_ptr() ) < 0 { child_fail( b"chdir\n" ); }

		libc::clearenv();

		let path_v = CString::new( "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" ).unwrap();
		let term_v = CString::new( "linux" ).unwrap();
		let vt_v = CString::new( vt.to_string() ).unwrap();
		setenv_kv( b"PATH\0",    path_v.as_ptr() );
		setenv_kv( b"TERM\0",    term_v.as_ptr() );
		setenv_kv( b"HOME\0",    home.as_ptr()   );
		setenv_kv( b"USER\0",    name.as_ptr()   );
		setenv_kv( b"LOGNAME\0", name.as_ptr()   );
		setenv_kv( b"SHELL\0",   shell.as_ptr() );
		setenv_kv( b"XDG_VTNR\0", vt_v.as_ptr()  );

		for kv in &pam_env
		{
			let bytes = kv.to_bytes();
			if let Some( eq ) = bytes.iter().position( |&b| b == b'=' )
			{
				let key   = CString::new( &bytes[..eq] ).unwrap();
				let value = CString::new( &bytes[eq + 1..] ).unwrap();
				libc::setenv( key.as_ptr(), value.as_ptr(), 1 );
			}
		}

		// Config-provided env layers on top — overrides PAM-injected values
		// if a key collides (e.g. XDG_SESSION_TYPE).
		let mut iter = env.iter();
		while let ( Some( k ), Some( v ) ) = ( iter.next(), iter.next() )
		{
			libc::setenv( k.as_ptr(), v.as_ptr(), 1 );
		}

		// Build a NULL-terminated argv pointer array for execv. The
		// CStrings in `argv` are borrowed from the parent stack frame,
		// which is still valid in the post-fork child.
		let mut argv_ptrs: Vec<*const libc::c_char> = argv.iter()
			.map( |s| s.as_ptr() )
			.collect();
		argv_ptrs.push( std::ptr::null() );

		libc::execv( command.as_ptr(), argv_ptrs.as_ptr() );
		child_fail( b"execv\n" );
	}
}

unsafe fn setenv_kv( name_with_nul: &[u8], value_ptr: *const libc::c_char )
{
	unsafe
	{
		libc::setenv( name_with_nul.as_ptr() as *const libc::c_char, value_ptr, 1 );
	}
}

fn child_fail_str( msg: &str ) -> !
{
	unsafe
	{
		let bytes = msg.as_bytes();
		let _ = libc::write( 2, bytes.as_ptr() as *const libc::c_void, bytes.len() );
		libc::_exit( 127 )
	}
}

fn child_fail( msg: &[u8] ) -> !
{
	unsafe
	{
		let _ = libc::write( 2, msg.as_ptr() as *const libc::c_void, msg.len() );
		libc::_exit( 127 )
	}
}
