// IPC with the privileged backend (`eydos-installer daemon`).
//
// Newline-delimited JSON over a Unix socket: the client sends one
// [`Request`] and reads a stream of [`Event`]s. The wizard runs as the
// session user; everything that needs root (partitioning, mkfs, mount,
// chroot, marker swaps) happens on the daemon side of this socket.

use std::io::{ BufRead, BufReader, Write };
use std::os::unix::net::UnixStream;
use std::sync::{ Arc, Mutex };

use crate::install::{ InstallPlan, InstallSlot, InstallStatus, Phase };

pub const SOCKET_PATH: &str = "/run/eydos-installer/daemon.sock";

#[ derive( Debug, serde::Serialize, serde::Deserialize ) ]
#[ serde( tag = "cmd", rename_all = "snake_case" ) ]
pub enum Request
{
	Install { plan: InstallPlan },
	CancelInstall,
	DiskOps { ops: Vec<RemoteDiskOp> },
	ApplyLocale { locale: String },
	StageAccount
	{
		username: String,
		hostname: String,
		password: String,
		encrypt:  bool,
	},
	FinishConfigure,
}

/// Disk-management operation resolved to stable device identifiers,
/// executable on either side of the socket. Emitted in apply order:
/// resizes, creates, deletes, wipes.
#[ derive( Clone, Debug, serde::Serialize, serde::Deserialize ) ]
pub enum RemoteDiskOp
{
	Wipe   { disk: std::path::PathBuf },
	Delete { disk: std::path::PathBuf, numbers: Vec<u32> },
	Resize
	{
		disk:          std::path::PathBuf,
		part:          std::path::PathBuf,
		number:        u32,
		current_bytes: u64,
		new_bytes:     u64,
		fs:            Option<String>,
	},
	Create { disk: std::path::PathBuf, start_byte: u64, size_bytes: u64 },
}

#[ derive( Debug, serde::Serialize, serde::Deserialize ) ]
#[ serde( tag = "event", rename_all = "snake_case" ) ]
pub enum Event
{
	Progress { phase: Phase, fraction: f32 },
	Done,
	Failed { message: String },
}

/// Start an install. Simulated plans always run in-process; real plans
/// go through the daemon. When the daemon is unreachable the engine
/// runs in-process only for root (development runs) — as the session
/// user that path can only die on EACCES against the block device, so
/// it fails fast with the "no-daemon" token instead.
pub fn start( plan: InstallPlan ) -> InstallSlot
{
	if plan.test { return crate::install::start( plan ); }
	match UnixStream::connect( SOCKET_PATH )
	{
		Ok( stream ) => start_remote( stream, plan ),
		Err( e ) if is_root() =>
		{
			eprintln!( "ipc: daemon unreachable ({e}); running the engine in-process" );
			crate::install::start( plan )
		}
		Err( e ) =>
		{
			eprintln!( "ipc: daemon unreachable ({e}) and not root" );
			Arc::new( Mutex::new( InstallStatus
			{
				phase:    Phase::Partition,
				fraction: 0.0,
				done:     false,
				error:    Some( "no-daemon".to_string() ),
			} ) )
		}
	}
}

fn is_root() -> bool
{
	use std::os::unix::fs::MetadataExt;
	std::fs::metadata( "/proc/self" ).map( |m| m.uid() == 0 ).unwrap_or( false )
}

fn start_remote( stream: UnixStream, plan: InstallPlan ) -> InstallSlot
{
	let slot: InstallSlot = Arc::new( Mutex::new( InstallStatus
	{
		phase:    Phase::Partition,
		fraction: 0.0,
		done:     false,
		error:    None,
	} ) );

	let slot_thread = slot.clone();
	std::thread::spawn( move ||
	{
		if let Err( e ) = drive_remote( stream, plan, &slot_thread )
		{
			if let Ok( mut status ) = slot_thread.lock()
			{
				if !status.done && status.error.is_none()
				{
					status.error = Some( e );
				}
			}
		}
	} );
	slot
}

fn drive_remote( stream: UnixStream, plan: InstallPlan, slot: &InstallSlot ) -> Result<(), String>
{
	let mut writer = stream.try_clone().map_err( |e| format!( "clone socket: {e}" ) )?;
	send( &mut writer, &Request::Install { plan } )?;

	let reader = BufReader::new( stream );
	for line in reader.lines()
	{
		let line = line.map_err( |e| format!( "read from daemon: {e}" ) )?;
		if line.trim().is_empty() { continue; }
		let event: Event = serde_json::from_str( &line )
			.map_err( |e| format!( "bad event from daemon: {e}" ) )?;
		match event
		{
			Event::Progress { phase, fraction } =>
			{
				if let Ok( mut status ) = slot.lock()
				{
					status.phase    = phase;
					status.fraction = fraction;
				}
			}
			Event::Done =>
			{
				if let Ok( mut status ) = slot.lock()
				{
					status.fraction = 1.0;
					status.done     = true;
				}
				return Ok( () );
			}
			Event::Failed { message } =>
			{
				if let Ok( mut status ) = slot.lock()
				{
					status.error = Some( message );
				}
				return Ok( () );
			}
		}
	}
	Err( "connection to the install daemon lost".to_string() )
}

/// Commit queued disk-management operations. The wizard cannot open
/// block devices, so they go to the daemon; when it is unreachable
/// (root development runs) they execute in-process.
pub fn apply_disk_ops( ops: &[ RemoteDiskOp ] ) -> Vec<String>
{
	if ops.is_empty() { return Vec::new(); }
	if UnixStream::connect( SOCKET_PATH ).is_err()
	{
		if is_root()
		{
			return crate::install::apply_disk_ops( ops );
		}
		return vec![ "no-daemon".to_string() ];
	}
	match roundtrip( &Request::DiskOps { ops: ops.to_vec() } )
	{
		Ok( () )   => Vec::new(),
		Err( msg ) => vec![ msg ],
	}
}

/// Abort the running install at its next checkpoint, wherever the
/// engine runs: locally (test / root fallback) and, best-effort, in
/// the daemon.
pub fn cancel_install()
{
	crate::install::request_cancel();
	if let Ok( mut stream ) = UnixStream::connect( SOCKET_PATH )
	{
		let _ = send( &mut stream, &Request::CancelInstall );
	}
}

/// Swap the lifecycle markers at the end of the Configure flow via the
/// daemon (they live in a root-owned directory).
pub fn finish_configure() -> Result<(), String>
{
	roundtrip( &Request::FinishConfigure )
}

/// Apply the wizard's language to the running system via the daemon
/// (falls back to a direct in-process attempt for root dev runs).
pub fn apply_locale( code: &str ) -> Result<(), String>
{
	if UnixStream::connect( SOCKET_PATH ).is_err()
	{
		crate::install::apply_locale( std::path::Path::new( "/" ), code );
		return Ok( () );
	}
	roundtrip( &Request::ApplyLocale { locale: code.to_string() } )
}

/// Stage the account (and optionally root re-encryption) for the next
/// boot via the daemon; the Configure flow cannot rename its own
/// logged-in user nor encrypt the mounted root.
pub fn stage_account( username: &str, hostname: &str, password: &str, encrypt: bool ) -> Result<(), String>
{
	roundtrip( &Request::StageAccount
	{
		username: username.to_string(),
		hostname: hostname.to_string(),
		password: password.to_string(),
		encrypt,
	} )
}

fn roundtrip( request: &Request ) -> Result<(), String>
{
	let stream = UnixStream::connect( SOCKET_PATH )
		.map_err( |e| format!( "connect {SOCKET_PATH}: {e}" ) )?;
	let mut writer = stream.try_clone().map_err( |e| format!( "clone socket: {e}" ) )?;
	send( &mut writer, request )?;

	let reader = BufReader::new( stream );
	for line in reader.lines()
	{
		let line = line.map_err( |e| format!( "read from daemon: {e}" ) )?;
		if line.trim().is_empty() { continue; }
		match serde_json::from_str( &line ).map_err( |e| format!( "bad event from daemon: {e}" ) )?
		{
			Event::Done                => return Ok( () ),
			Event::Failed { message }  => return Err( message ),
			Event::Progress { .. }     => continue,
		}
	}
	Err( "connection to the install daemon lost".to_string() )
}

pub fn send<W: Write>( writer: &mut W, request: &impl serde::Serialize ) -> Result<(), String>
{
	let mut line = serde_json::to_string( request ).map_err( |e| format!( "encode request: {e}" ) )?;
	line.push( '\n' );
	writer.write_all( line.as_bytes() ).map_err( |e| format!( "write to daemon: {e}" ) )
}
