mod auth;
mod config;
mod greeter;
mod pam_session;
mod server;
mod session;
mod vt;

use anyhow::{ anyhow, bail, Result };
use loginmanager_ipc::{
	proto::{ Request, Response },
	wire::{ read_message, write_message },
	DEFAULT_SOCKET_PATH,
};
use std::path::{ Path, PathBuf };

const DEFAULT_PAM_SERVICE: &str = "loginmanager-daemon";

fn usage() -> !
{
	eprintln!( "loginmanager-daemon — privileged backend for the Eydos greeter" );
	eprintln!();
	eprintln!( "USAGE:" );
	eprintln!( "    loginmanager-daemon auth <username> [--pam-service <name>]" );
	eprintln!( "    loginmanager-daemon vt-info" );
	eprintln!( "    loginmanager-daemon spawn <username> [--pam-service <name>] [--shell <path>] [--config <path>]" );
	eprintln!( "    loginmanager-daemon run    [--socket <path>] [--config <path>]" );
	eprintln!( "    loginmanager-daemon client <username> [--socket <path>]" );
	eprintln!();
	eprintln!( "spawn: authenticate, pam_open_session, reserve a free VT, fork, drop" );
	eprintln!( "       privileges in the child, exec the shell on the new VT, then" );
	eprintln!( "       VT_ACTIVATE / VT_WAITACTIVE in the parent. The daemon waits for" );
	eprintln!( "       the child to exit and only then closes the PAM session." );
	eprintln!();
	eprintln!( "run:   start the IPC server on the Unix socket and accept greeter" );
	eprintln!( "       connections. Default socket: {DEFAULT_SOCKET_PATH}." );
	eprintln!();
	eprintln!( "client: stdin-driven IPC client for debugging the server. Connects" );
	eprintln!( "       to the socket, sends BeginAuth, walks through any challenges" );
	eprintln!( "       by prompting stdin, and reports the final response." );
	eprintln!();
	eprintln!( "Default PAM service: {DEFAULT_PAM_SERVICE}." );
	eprintln!( "Default config:      {}", config::DEFAULT_PATH );
	std::process::exit( 2 );
}

fn parse_pam_service( args: &[String] ) -> Result<String>
{
	let mut iter = args.iter();
	while let Some( a ) = iter.next()
	{
		if a == "--pam-service"
		{
			match iter.next()
			{
				Some( v ) => return Ok( v.clone() ),
				None      => bail!( "--pam-service requires a value" ),
			}
		}
	}
	Ok( DEFAULT_PAM_SERVICE.to_string() )
}

fn parse_shell( args: &[String] ) -> Result<Option<String>>
{
	let mut iter = args.iter();
	while let Some( a ) = iter.next()
	{
		if a == "--shell"
		{
			match iter.next()
			{
				Some( v ) => return Ok( Some( v.clone() ) ),
				None      => bail!( "--shell requires a value" ),
			}
		}
	}
	Ok( None )
}

fn parse_socket( args: &[String] ) -> Result<String>
{
	let mut iter = args.iter();
	while let Some( a ) = iter.next()
	{
		if a == "--socket"
		{
			match iter.next()
			{
				Some( v ) => return Ok( v.clone() ),
				None      => bail!( "--socket requires a value" ),
			}
		}
	}
	Ok( DEFAULT_SOCKET_PATH.to_string() )
}

fn parse_config_path( args: &[String] ) -> Result<PathBuf>
{
	let mut iter = args.iter();
	while let Some( a ) = iter.next()
	{
		if a == "--config"
		{
			match iter.next()
			{
				Some( v ) => return Ok( PathBuf::from( v ) ),
				None      => bail!( "--config requires a value" ),
			}
		}
	}
	Ok( PathBuf::from( config::DEFAULT_PATH ) )
}

fn load_config( args: &[String] ) -> Result<config::DaemonConfig>
{
	let path = parse_config_path( args )?;
	config::load( Path::new( &path ) )
}

fn cmd_auth( username: &str, service: &str ) -> Result<()>
{
	tracing::info!( "auth: service={service} user={username}" );

	let password = rpassword::prompt_password( "Password: " )?;

	auth::authenticate( service, username, &password )?;

	tracing::info!( "auth: success" );
	println!( "Authentication successful for {username}" );
	Ok( () )
}

fn cmd_vt_info() -> Result<()>
{
	let cur  = vt::current()?;
	let free = vt::open_query()?;
	println!( "current VT: {cur}" );
	println!( "next free VT: {free}" );
	Ok( () )
}

fn cmd_client( username: &str, socket_path: &str ) -> Result<()>
{
	let runtime = tokio::runtime::Builder::new_current_thread()
		.enable_all()
		.build()?;
	runtime.block_on( client_session( username.to_string(), socket_path.to_string() ) )
}

async fn client_session( username: String, socket_path: String ) -> Result<()>
{
	let stream = tokio::net::UnixStream::connect( &socket_path ).await
		.map_err( |e| anyhow!( "connect {socket_path}: {e}" ) )?;
	let ( mut reader, mut writer ) = stream.into_split();

	write_message( &mut writer, &Request::BeginAuth { username: username.clone() } ).await
		.map_err( |e| anyhow!( "BeginAuth: {e}" ) )?;

	loop
	{
		let resp: Response = read_message( &mut reader ).await
			.map_err( |e| anyhow!( "read response: {e}" ) )?;

		match resp
		{
			Response::AuthChallenge { prompt, secret } =>
			{
				let answer = if secret
				{
					rpassword::prompt_password( &prompt )?
				} else {
					use std::io::{ stdout, Write };
					print!( "{prompt}" );
					stdout().flush().ok();
					let mut line = String::new();
					std::io::stdin().read_line( &mut line )?;
					line.trim_end_matches( '\n' ).to_string()
				};
				write_message( &mut writer, &Request::AuthResponse { answer: Some( answer ) } ).await
					.map_err( |e| anyhow!( "AuthResponse: {e}" ) )?;
			}
			Response::AuthAccepted =>
			{
				println!( "[client] AuthAccepted — waiting for SessionReady" );
			}
			Response::AuthRejected { reason } =>
			{
				eprintln!( "[client] AuthRejected: {reason}" );
				return Ok( () );
			}
			Response::SessionReady { vt } =>
			{
				println!( "[client] SessionReady on VT {vt}" );
				return Ok( () );
			}
			Response::Error { description } =>
			{
				bail!( "server: {description}" );
			}
		}
	}
}

fn cmd_spawn(
	username: &str,
	service:  &str,
	shell:    Option<&str>,
	config:   &config::DaemonConfig,
) -> Result<()>
{
	let command = match shell
	{
		Some( s ) => PathBuf::from( s ),
		None      => config.session.command.clone(),
	};
	tracing::info!( "spawn: service={service} user={username} command={:?}", command );

	let password = rpassword::prompt_password( "Password: " )?;

	let status = session::spawn( service, username, &password, &command, &config.session.environment )?;

	let exit_code = if libc::WIFEXITED( status )
	{
		libc::WEXITSTATUS( status )
	} else if libc::WIFSIGNALED( status )
	{
		128 + libc::WTERMSIG( status )
	} else {
		-1
	};

	println!( "session ended (exit={exit_code})" );
	Ok( () )
}

fn main() -> Result<()>
{
	tracing_subscriber::fmt().with_target( false ).init();

	let args: Vec<String> = std::env::args().skip( 1 ).collect();
	if args.is_empty() { usage(); }

	match args[0].as_str()
	{
		"auth" =>
		{
			if args.len() < 2 { usage(); }
			let service = parse_pam_service( &args[2..] )?;
			cmd_auth( &args[1], &service )
		}
		"vt-info" => cmd_vt_info(),
		"spawn" =>
		{
			if args.len() < 2 { usage(); }
			let service = parse_pam_service( &args[2..] )?;
			let shell   = parse_shell( &args[2..] )?;
			let cfg     = load_config( &args[2..] )?;
			cmd_spawn( &args[1], &service, shell.as_deref(), &cfg )
		}
		"run" =>
		{
			let socket = parse_socket( &args[1..] )?;
			let cfg    = load_config( &args[1..] )?;
			tracing::info!(
				"run: starting IPC server on {socket}, session-command={:?}, greeter={:?}@VT{}",
				cfg.session.command, cfg.greeter.command, cfg.greeter.vt,
			);
			server::run( &socket, cfg )
		}
		"client" =>
		{
			if args.len() < 2 { usage(); }
			let socket = parse_socket( &args[2..] )?;
			cmd_client( &args[1], &socket )
		}
		_ => usage(),
	}
}
