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

//! Single-instance guard over the session bus (`org.eydos.Settings`). The
//! first instance owns the name and serves `Open( screen )`; every later
//! launch forwards its `--screen` argument to it, asks forge to raise the
//! existing window and exits.

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

use zbus::blocking::{ Connection, Proxy };

pub const APP_ID: &str = "eydos-settings";
const NAME: &str = "org.eydos.Settings";
const PATH: &str = "/org/eydos/Settings";

/// Deep links delivered by later launches, drained by `poll_external`. The
/// waker nudges the ltk event loop so navigation happens immediately instead
/// of on the next timer tick.
#[ derive( Default ) ]
pub struct Inbox
{
	pub pending: Vec<String>,
	pub waker:   Option<ltk::ChannelSender<crate::app::Msg>>,
}

struct Service
{
	inbox: Arc<Mutex<Inbox>>,
}

#[ zbus::interface( name = "org.eydos.Settings" ) ]
impl Service
{
	fn open( &self, screen: &str )
	{
		let mut inbox = self.inbox.lock().unwrap();
		inbox.pending.push( screen.to_string() );
		if let Some( waker ) = inbox.waker.as_ref()
		{
			let _ = waker.send( crate::app::Msg::Refresh );
		}
	}
}

pub enum Instance
{
	/// This process owns the bus name; keep the connection alive for the
	/// app's lifetime so the `Open` service stays reachable.
	Primary
	{
		inbox: Arc<Mutex<Inbox>>,
		conn:  Option<Connection>,
	},
	/// Another instance is running and has been told to take over.
	Secondary,
}

/// Claim the well-known name. Without a session bus the app simply runs
/// standalone (no single-instance guarantee, but fully functional).
pub fn claim( screen: Option<&str> ) -> Instance
{
	let inbox: Arc<Mutex<Inbox>> = Arc::default();
	let Ok( conn ) = Connection::session() else
	{
		return Instance::Primary { inbox, conn: None };
	};
	if conn.object_server().at( PATH, Service { inbox: inbox.clone() } ).is_err()
	{
		return Instance::Primary { inbox, conn: None };
	}
	match conn.request_name( NAME )
	{
		Ok( () ) => Instance::Primary { inbox, conn: Some( conn ) },
		Err( zbus::Error::NameTaken ) =>
		{
			let _ = Proxy::new( &conn, NAME, PATH, NAME )
				.and_then( |p| p.call::<_, _, ()>( "Open", &( screen.unwrap_or( "" ), ) ) );
			raise_existing_window();
			Instance::Secondary
		}
		Err( _ ) => Instance::Primary { inbox, conn: Some( conn ) },
	}
}

/// Ask forge to bring the running instance's window to the front, the same
/// `activate_app_id` message crustace's dock uses.
fn raise_existing_window()
{
	let Ok( runtime_dir ) = std::env::var( "XDG_RUNTIME_DIR" ) else { return };
	let Ok( display ) = std::env::var( "WAYLAND_DISPLAY" ) else { return };
	let path = format!( "{}/forge-{}.sock", runtime_dir, display );
	let Ok( mut stream ) = UnixStream::connect( &path ) else { return };
	let msg = format!( r#"{{"action":"activate_app_id","app_id":"{}"}}"#, APP_ID );
	let _ = stream.write_all( msg.as_bytes() );
}
