//! Push notifications to Evolution Data Server.
//!
//! After every D-Bus mutation that bumps `change_counter` we invoke
//! `Refresh()` on the per-source `org.gnome.evolution.dataserver.AddressBook`
//! interface. EDS responds by re-running its CardDAV sync against our
//! loopback endpoint, so gnome-contacts and Evolution see the change in
//! seconds rather than waiting for `getctag` polling (~30 minutes by
//! default).
//!
//! Best-effort: any failure (EDS not running, the source not yet known,
//! a method that doesn't exist on this EDS version) is logged at `debug`
//! and swallowed — push must never break a mutation that already
//! succeeded in our DB.
//!
//! ## Resolving the target
//!
//! `RefreshBackend` on the `SourceManager` only refreshes collection
//! sources (it re-runs WebDAV discovery), not the per-addressbook sync.
//! For per-addressbook refresh we go through the
//! `AddressBookFactory`:
//!
//! 1. Discover the factory bus name. EDS exposes it as
//!    `org.gnome.evolution.dataserver.AddressBook<N>` where `N` varies
//!    per release. We list bus names on the daemon and pick the highest
//!    `N` that's running.
//! 2. Call `AddressBookFactory.OpenAddressBook(uid)` → `(o path, s bus_name)`.
//!    EDS spawns the backend the first time and reuses it after.
//! 3. Call `AddressBook.Refresh()` at the returned path on the returned
//!    bus name.
//!
//! Steps 1-2 are cached. On any failure we drop the cache and re-resolve
//! next call — handles the case where EDS or the factory restarts.

use std::sync::{ Arc, Mutex };

use anyhow::{ Context, Result, anyhow };

const ADDRESS_BOOK_FACTORY_BUS_PREFIX: &str = "org.gnome.evolution.dataserver.AddressBook";
const ADDRESS_BOOK_FACTORY_PATH:       &str = "/org/gnome/evolution/dataserver/AddressBookFactory";
const ADDRESS_BOOK_FACTORY_INTERFACE:  &str = "org.gnome.evolution.dataserver.AddressBookFactory";
const ADDRESS_BOOK_INTERFACE:          &str = "org.gnome.evolution.dataserver.AddressBook";

/// The address book source UID — must match the file name of
/// `liberux-contacts.source`.
pub const ADDRESS_BOOK_SOURCE_UID: &str = "liberux-contacts";

#[ derive( Debug, Clone ) ]
struct ResolvedTarget
{
	bus_name: String,
	path:     String,
}

#[ derive( Debug ) ]
pub struct EdsPusher
{
	bus:        zbus::Connection,
	target_uid: String,
	cached:     Mutex<Option<ResolvedTarget>>,
}

impl EdsPusher
{
	pub fn for_address_book( bus: zbus::Connection ) -> Result<Self>
	{
		Ok( Self
		{
			bus,
			target_uid: ADDRESS_BOOK_SOURCE_UID.to_owned(),
			cached:     Mutex::new( None ),
		} )
	}

	pub fn into_arc( self ) -> Arc<Self>
	{
		Arc::new( self )
	}

	/// Best-effort EDS refresh. Errors are logged at `debug` and dropped.
	pub async fn refresh( &self )
	{
		let cached = self.cached.lock().unwrap().clone();
		if let Some( t ) = cached
		{
			match self.try_refresh( &t ).await
			{
				Ok( () ) =>
				{
					log::debug!(
						"EDS AddressBook.Refresh() OK at cached {} {}",
						t.bus_name, t.path,
					);
					return;
				}
				Err( e ) =>
				{
					log::debug!(
						"EDS Refresh on cached {} failed ({:?}); re-resolving",
						t.path, e,
					);
					*self.cached.lock().unwrap() = None;
				}
			}
		}

		match self.resolve().await
		{
			Ok( t ) =>
			{
				match self.try_refresh( &t ).await
				{
					Ok( () ) =>
					{
						log::debug!(
							"EDS AddressBook.Refresh() OK at {} {}",
							t.bus_name, t.path,
						);
						*self.cached.lock().unwrap() = Some( t );
					}
					Err( e ) => log::debug!( "EDS Refresh after resolve failed: {:#}", e ),
				}
			}
			Err( e ) => log::debug!( "EDS AddressBook resolve failed: {:#}", e ),
		}
	}

	async fn try_refresh( &self, t: &ResolvedTarget ) -> Result<()>
	{
		self.bus
			.call_method(
				Some( t.bus_name.as_str() ),
				t.path.as_str(),
				Some( ADDRESS_BOOK_INTERFACE ),
				"Refresh",
				&(),
			)
			.await
			.with_context( || format!( "Refresh() on {} {}", t.bus_name, t.path ) )?;
		Ok( () )
	}

	async fn resolve( &self ) -> Result<ResolvedTarget>
	{
		let factory_bus = self.find_factory_bus_name().await
			.context( "locating AddressBook factory bus" )?;
		let ( path, bus_name ) = self.open_address_book( &factory_bus ).await
			.with_context( || format!(
				"AddressBookFactory.OpenAddressBook({:?})",
				self.target_uid,
			) )?;
		Ok( ResolvedTarget { bus_name, path } )
	}

	/// Pick the highest-numbered `org.gnome.evolution.dataserver.AddressBook<N>`
	/// bus name currently registered with the session bus daemon.
	async fn find_factory_bus_name( &self ) -> Result<String>
	{
		let reply = self.bus
			.call_method(
				Some( "org.freedesktop.DBus" ),
				"/org/freedesktop/DBus",
				Some( "org.freedesktop.DBus" ),
				"ListNames",
				&(),
			)
			.await
			.context( "ListNames" )?;
		let names: Vec<String> = reply.body()
			.deserialize()
			.context( "decoding ListNames reply" )?;

		let mut candidates: Vec<( u32, String )> = names.into_iter()
			.filter_map( |n|
			{
				let v: u32 = n.strip_prefix( ADDRESS_BOOK_FACTORY_BUS_PREFIX )
					.and_then( |s| s.parse().ok() )?;
				Some( ( v, n ) )
			} )
			.collect();
		candidates.sort_by_key( |( v, _ )| *v );

		candidates.pop()
			.map( |( _, n )| n )
			.ok_or_else( || anyhow!(
				"no org.gnome.evolution.dataserver.AddressBook<N> bus name found",
			) )
	}

	async fn open_address_book(
		&self,
		factory_bus: &str,
	) -> Result<( String, String )>
	{
		let reply = self.bus
			.call_method(
				Some( factory_bus ),
				ADDRESS_BOOK_FACTORY_PATH,
				Some( ADDRESS_BOOK_FACTORY_INTERFACE ),
				"OpenAddressBook",
				&( self.target_uid.as_str(), ),
			)
			.await
			.context( "OpenAddressBook" )?;
		// Modern EDS returns `(s s)` — `(object_path_as_string, bus_name)` —
		// rather than the older `(o s)`. Both elements are plain strings on
		// the wire; the first carries the object path of the per-source
		// AddressBook backend on the second's bus name.
		let pair: ( String, String ) = reply.body()
			.deserialize()
			.context( "decoding OpenAddressBook reply" )?;
		Ok( pair )
	}
}
