// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>

//! Session restore: the list of applications that were open when the
//! session ended, persisted on logout / reboot / power off and relaunched
//! at the next start.
//!
//! The file lives at `$XDG_STATE_HOME/crustace/session.json` and is
//! consumed (deleted) as soon as it is read, so a failed relaunch can never
//! turn into a relaunch loop.

use std::collections::VecDeque;
use std::fs;
use std::io::Write;
use std::path::PathBuf;

use serde::{ Deserialize, Serialize };

use crate::powermenu::PowerAction;

/// Environment variable set on relaunched children so ltk applications
/// restore their internal state.
pub const RESTORE_ENV: &str = "LTK_SESSION_RESTORE";

const VERSION: u32 = 1;

#[ derive( Serialize, Deserialize ) ]
struct SessionFile
{
	version:  u32,
	saved_at: String,
	reason:   String,
	apps:     Vec<String>,
}

fn state_path() -> Option<PathBuf>
{
	dirs::state_dir()
		.or_else( || dirs::home_dir().map( |h| h.join( ".local" ).join( "state" ) ) )
		.map( |d| d.join( "crustace" ).join( "session.json" ) )
}

fn reason_name( reason: PowerAction ) -> &'static str
{
	match reason
	{
		PowerAction::Logout   => "logout",
		PowerAction::Reboot   => "reboot",
		PowerAction::PowerOff => "poweroff",
		PowerAction::Suspend  => "suspend",
		PowerAction::Lock     => "lock",
		PowerAction::Cancel   => "cancel",
	}
}

/// Persist `apps` (desktop ids, in dock order) synchronously: the Logout
/// path calls `process::exit` right after, so the data must be on disk
/// when this returns.
pub fn save( reason: PowerAction, apps: &[ String ] ) -> std::io::Result<()>
{
	let Some( path ) = state_path() else
	{
		return Err( std::io::Error::other( "no state directory" ) );
	};
	if let Some( parent ) = path.parent()
	{
		fs::create_dir_all( parent )?;
	}
	let file = SessionFile {
		version:  VERSION,
		saved_at: chrono::Utc::now().to_rfc3339(),
		reason:   reason_name( reason ).to_string(),
		apps:     apps.to_vec(),
	};
	let json = serde_json::to_vec_pretty( &file ).map_err( std::io::Error::other )?;
	let tmp = path.with_extension( "json.tmp" );
	{
		let mut f = fs::File::create( &tmp )?;
		f.write_all( &json )?;
		f.sync_all()?;
	}
	fs::rename( &tmp, &path )
}

/// Read the saved list and delete the file. `None` when there is nothing
/// to restore or the file is unreadable.
pub fn take() -> Option<Vec<String>>
{
	let path = state_path()?;
	let data = fs::read( &path ).ok()?;
	let _ = fs::remove_file( &path );
	let file: SessionFile = serde_json::from_slice( &data ).ok()?;
	if file.version != VERSION
	{
		return None;
	}
	Some( file.apps )
}

/// Sequential relaunch queue: one app in flight at a time, so activation
/// tokens never collide and cold starts do not pile up.
#[ derive( Default ) ]
pub struct RestoreQueue
{
	pending:       VecDeque<String>,
	in_flight:     Option<String>,
	pub generation: u32,
}

impl RestoreQueue
{
	pub fn new() -> Self { Self::default() }

	pub fn seed( &mut self, apps: Vec<String> )
	{
		self.pending = apps.into();
		self.in_flight = None;
		self.generation = self.generation.wrapping_add( 1 );
	}

	pub fn is_empty( &self ) -> bool
	{
		self.pending.is_empty() && self.in_flight.is_none()
	}

	/// Advance to the next app; returns the desktop id to launch, `None`
	/// when the queue is drained.
	pub fn next( &mut self ) -> Option<String>
	{
		self.generation = self.generation.wrapping_add( 1 );
		self.in_flight = self.pending.pop_front();
		self.in_flight.clone()
	}

	/// The in-flight app mapped its first toplevel; returns `true` when
	/// the queue should advance right away.
	pub fn on_mapped( &mut self, desktop_id: &str ) -> bool
	{
		self.in_flight.as_deref() == Some( desktop_id )
	}
}

#[ cfg( test ) ]
mod tests
{
	use super::*;

	#[ test ]
	fn queue_advances_in_order()
	{
		let mut q = RestoreQueue::new();
		q.seed( vec![ "a".into(), "b".into() ] );
		assert!( !q.is_empty() );
		assert_eq!( q.next().as_deref(), Some( "a" ) );
		assert!( q.on_mapped( "a" ) );
		assert!( !q.on_mapped( "b" ) );
		assert_eq!( q.next().as_deref(), Some( "b" ) );
		assert_eq!( q.next(), None );
		assert!( q.is_empty() );
	}

	#[ test ]
	fn generation_changes_on_every_step()
	{
		let mut q = RestoreQueue::new();
		let g0 = q.generation;
		q.seed( vec![ "a".into() ] );
		let g1 = q.generation;
		q.next();
		let g2 = q.generation;
		assert!( g0 != g1 && g1 != g2 );
	}
}
