//! Linux VT (virtual terminal) ioctl helpers.
//!
//! All operations require CAP_SYS_TTY_CONFIG (i.e. root) and act on the
//! current console (`/dev/tty0`), which always refers to the foreground VT
//! of the seat — not the caller's controlling tty.

use anyhow::{ anyhow, bail, Result };
use std::fs::{ File, OpenOptions };
use std::os::unix::io::AsRawFd;

const VT_OPENQRY:    libc::c_ulong = 0x5600;
const VT_GETSTATE:   libc::c_ulong = 0x5603;
const VT_ACTIVATE:   libc::c_ulong = 0x5606;
const VT_WAITACTIVE: libc::c_ulong = 0x5607;
const VT_SETMODE:    libc::c_ulong = 0x5602;
const KDSETMODE:     libc::c_ulong = 0x4B3A;
const KD_TEXT:       libc::c_long  = 0x00;
const KD_GRAPHICS:   libc::c_long  = 0x01;
const VT_AUTO:       libc::c_char  = 0x00;

#[ repr( C ) ]
struct VtMode
{
	mode:   libc::c_char,
	waitv:  libc::c_char,
	relsig: libc::c_short,
	acqsig: libc::c_short,
	frsig:  libc::c_short,
}

#[ repr( C ) ]
struct VtStat
{
	v_active: libc::c_ushort,
	v_signal: libc::c_ushort,
	v_state:  libc::c_ushort,
}

fn open_console() -> Result<File>
{
	OpenOptions::new()
		.read( true ).write( true )
		.open( "/dev/tty0" )
		.map_err( |e| anyhow!( "open /dev/tty0 (needs root / CAP_SYS_TTY_CONFIG): {e}" ) )
}

pub fn current() -> Result<u16>
{
	let tty = open_console()?;
	let mut state = VtStat { v_active: 0, v_signal: 0, v_state: 0 };
	let rc = unsafe { libc::ioctl( tty.as_raw_fd(), VT_GETSTATE, &mut state as *mut VtStat ) };
	if rc < 0 { bail!( "VT_GETSTATE: {}", std::io::Error::last_os_error() ); }
	Ok( state.v_active )
}

pub fn open_query() -> Result<u32>
{
	let tty = open_console()?;
	let mut n: libc::c_int = 0;
	let rc = unsafe { libc::ioctl( tty.as_raw_fd(), VT_OPENQRY, &mut n as *mut libc::c_int ) };
	if rc < 0 { bail!( "VT_OPENQRY: {}", std::io::Error::last_os_error() ); }
	if n < 0 { bail!( "VT_OPENQRY: no free VT available" ); }
	Ok( n as u32 )
}

#[ allow( dead_code ) ]
pub fn activate( vt: u32 ) -> Result<()>
{
	let tty = open_console()?;
	let rc = unsafe { libc::ioctl( tty.as_raw_fd(), VT_ACTIVATE, vt as libc::c_ulong ) };
	if rc < 0 { bail!( "VT_ACTIVATE( {vt} ): {}", std::io::Error::last_os_error() ); }
	Ok( () )
}

pub fn wait_active( vt: u32 ) -> Result<()>
{
	let tty = open_console()?;
	let rc = unsafe { libc::ioctl( tty.as_raw_fd(), VT_WAITACTIVE, vt as libc::c_ulong ) };
	if rc < 0 { bail!( "VT_WAITACTIVE( {vt} ): {}", std::io::Error::last_os_error() ); }
	Ok( () )
}

/// Force a dead compositor's old VT back into a state the kernel is willing
/// to switch away from.
///
/// Wayland compositors like forge put their VT in `KD_GRAPHICS` mode (so the
/// kernel doesn't paint console text on top of the DRM scanout) and register
/// `VT_SETMODE = VT_PROCESS` so the kernel asks them politely before changing
/// VT. When the compositor dies abruptly, the kernel cleans up the process
/// reference (`VT_PROCESS` reverts to `VT_AUTO`) but **leaves the VT in
/// `KD_GRAPHICS`** — and `set_console()` then refuses to switch with `-EINVAL`
/// for the combo `VT_AUTO && KD_GRAPHICS`, silently dropping our request.
/// `VT_ACTIVATE` returns 0 without doing anything, `VT_WAITACTIVE` hangs.
///
/// Resetting the source VT to `KD_TEXT` (and re-asserting `VT_AUTO` just in
/// case) unblocks the kernel so the subsequent `SwitchTo`/`VT_ACTIVATE`
/// actually lands.
pub fn reset_vt_for_switch( vt: u32 ) -> Result<()>
{
	let path = format!( "/dev/tty{}", vt );
	let tty  = OpenOptions::new()
		.read( true ).write( true )
		.open( &path )
		.map_err( |e| anyhow!( "open {path}: {e}" ) )?;

	let rc = unsafe { libc::ioctl( tty.as_raw_fd(), KDSETMODE, KD_TEXT ) };
	if rc < 0
	{
		let e = std::io::Error::last_os_error();
		tracing::warn!( "KDSETMODE(KD_TEXT) on tty{vt}: {e}" );
	}

	let vt_auto = VtMode { mode: VT_AUTO, waitv: 0, relsig: 0, acqsig: 0, frsig: 0 };
	let rc = unsafe { libc::ioctl( tty.as_raw_fd(), VT_SETMODE, &vt_auto as *const VtMode ) };
	if rc < 0
	{
		let e = std::io::Error::last_os_error();
		tracing::warn!( "VT_SETMODE(VT_AUTO) on tty{vt}: {e}" );
	}
	Ok( () )
}

/// Put `/dev/tty<vt>` into `KD_GRAPHICS` mode. Stops the kernel's fbcon
/// driver from drawing the text console on that VT — important for the
/// brief window between the splash dropping DRM master and forge taking
/// it back, where fbcon would otherwise re-modeset to text mode and the
/// user sees a flash of console output before the user session lands.
///
/// Idempotent: a forge that takes master afterwards (re)sets the same
/// mode on the way to its compositor flow.
pub fn set_graphics_mode( vt: u32 ) -> Result<()>
{
	let path = format!( "/dev/tty{}", vt );
	let tty  = OpenOptions::new()
		.read( true ).write( true )
		.open( &path )
		.map_err( |e| anyhow!( "open {path}: {e}" ) )?;

	let rc = unsafe { libc::ioctl( tty.as_raw_fd(), KDSETMODE, KD_GRAPHICS ) };
	if rc < 0
	{
		let e = std::io::Error::last_os_error();
		bail!( "KDSETMODE(KD_GRAPHICS) on tty{vt}: {e}" );
	}
	Ok( () )
}

/// Ask systemd-logind to switch the seat to `vt`. Unlike the raw ioctl,
/// logind pauses the outgoing session's libseat devices (releasing DRM
/// master) **before** the VT switch and resumes the new one synchronously,
/// so the next compositor gets the GPU without a race.
pub fn switch_via_logind( seat: &str, vt: u32 ) -> Result<()>
{
	let output = std::process::Command::new( "busctl" )
		.args( [
			"call",
			"org.freedesktop.login1",
			&format!( "/org/freedesktop/login1/seat/{seat}" ),
			"org.freedesktop.login1.Seat",
			"SwitchTo",
			"u",
			&vt.to_string(),
		] )
		.output()
		.map_err( |e| anyhow!( "spawn busctl: {e}" ) )?;
	if !output.status.success()
	{
		bail!(
			"busctl SwitchTo( seat={seat}, vt={vt} ): {}",
			String::from_utf8_lossy( &output.stderr ).trim()
		);
	}
	Ok( () )
}
