//! Evolution Data Server auto-registration.
//!
//! On first launch (and on every restart where the bound CardDAV port
//! has changed) we drop two GKeyFile-formatted `.source` files into
//! `$XDG_CONFIG_HOME/evolution/sources/`:
//!
//! - `liberux-contacts.source` — the address book itself.
//! - `liberux-contacts-collection.source` — the parent "Liberux" account
//!   collection that groups it (so EDS surfaces it as one logical account
//!   in the UI rather than a loose CardDAV book).
//!
//! Behaviour:
//!
//! - If `~/.config/evolution/sources/` does not exist, EDS is considered
//!   absent and we no-op (with an `info` log).
//! - If `~/.config/liberux/contacts/no-eds-autoregister` exists, the user
//!   has opted out and we leave any pre-existing `.source` files alone.
//! - Otherwise we compare the desired and on-disk contents; identical →
//!   leave the file's mtime alone (so `evolution-source-registry` doesn't
//!   needlessly reload), different or absent → atomically write.
//!
//! **Status:** the GKeyFile schema below is a *candidate* (see
//! `docs/eds-source.md`). It needs to be cross-checked against a real EDS
//! install before we declare Phase 5 done.

use std::fs;
use std::io::Write;
use std::os::unix::fs::OpenOptionsExt;
use std::path::{ Path, PathBuf };

use anyhow::{ Context, Result };

use crate::config::Paths;

const ADDRESS_BOOK_UID: &str = "liberux-contacts";
const COLLECTION_UID: &str   = "liberux-contacts-collection";

#[ derive( Debug ) ]
pub enum RegistrationOutcome
{
	/// EDS sources directory missing — EDS is not installed (or not
	/// configured). Auto-registration is silently skipped.
	NotInstalled,

	/// The opt-out marker file is present; we left every `.source` file
	/// untouched.
	OptedOut,

	/// One or both `.source` files were written or refreshed.
	Registered
	{
		wrote:     Vec<PathBuf>,
		untouched: Vec<PathBuf>,
	},
}

/// Best-effort registration. The caller logs the outcome and continues
/// regardless — CardDAV still works for clients that add the URL by hand.
pub fn register( paths: &Paths, port: u16 ) -> Result<RegistrationOutcome>
{
	if !paths.eds_sources_dir.exists()
	{
		log::info!(
			"EDS not detected ({} missing); skipping address book auto-registration",
			paths.eds_sources_dir.display(),
		);
		return Ok( RegistrationOutcome::NotInstalled );
	}

	if paths.no_eds_autoregister.exists()
	{
		log::info!(
			"EDS auto-registration opt-out present at {}; leaving existing .source files alone",
			paths.no_eds_autoregister.display(),
		);
		return Ok( RegistrationOutcome::OptedOut );
	}

	let address_path    = paths.eds_sources_dir.join( format!( "{}.source", ADDRESS_BOOK_UID ) );
	let collection_path = paths.eds_sources_dir.join( format!( "{}.source", COLLECTION_UID ) );

	let address_body    = render_address_book_source( port );
	let collection_body = render_collection_source( port );

	let mut wrote     = Vec::new();
	let mut untouched = Vec::new();

	write_if_changed( &address_path,    &address_body,    &mut wrote, &mut untouched )?;
	write_if_changed( &collection_path, &collection_body, &mut wrote, &mut untouched )?;

	Ok( RegistrationOutcome::Registered { wrote, untouched } )
}

fn render_address_book_source( port: u16 ) -> String
{
	format!( r#"[Data Source]
DisplayName=Liberux Contacts
Enabled=true
Parent={collection_uid}

[Authentication]
Host=127.0.0.1
Port={port}
ProxyUid=system-proxy
RememberPassword=true
User=liberux
Method=plain/password

[Security]
Method=none

[WebDAV Backend]
ResourcePath=/addressbook/main/
EmailAddress=
DisplayName=Liberux Contacts
Order=0

[Offline]
StaySynchronized=true
IncludeMe=true

[Address Book]
BackendName=carddav
Order=0
"#,
		collection_uid = COLLECTION_UID,
		port = port,
	)
}

fn render_collection_source( port: u16 ) -> String
{
	format!( r#"[Data Source]
DisplayName=Liberux
Enabled=true

[Authentication]
Host=127.0.0.1
Port={port}
ProxyUid=system-proxy
RememberPassword=true
User=liberux

[Collection]
BackendName=webdav
Identity=liberux@local
ContactsEnabled=true
CalendarEnabled=false
MailEnabled=false
"#,
		port = port,
	)
}

fn write_if_changed(
	path: &Path,
	expected: &str,
	wrote: &mut Vec<PathBuf>,
	untouched: &mut Vec<PathBuf>,
) -> Result<()>
{
	if let Ok( current ) = fs::read_to_string( path )
	{
		if current == expected
		{
			untouched.push( path.to_owned() );
			return Ok( () );
		}
	}

	let tmp = path.with_extension( "source.tmp" );
	{
		let mut f = fs::OpenOptions::new()
			.create( true )
			.write( true )
			.truncate( true )
			.mode( 0o644 )
			.open( &tmp )
			.with_context( || format!( "creating {}", tmp.display() ) )?;
		f.write_all( expected.as_bytes() )?;
		f.sync_all()?;
	}
	fs::rename( &tmp, path )
		.with_context( || format!( "renaming {} -> {}", tmp.display(), path.display() ) )?;
	wrote.push( path.to_owned() );
	Ok( () )
}

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

	#[ test ]
	fn skips_when_eds_dir_missing()
	{
		let root = tempdir().unwrap();
		let paths = Paths::under_root( root.path() );
		match register( &paths, 12345 ).unwrap()
		{
			RegistrationOutcome::NotInstalled => {}
			other => panic!( "expected NotInstalled, got {:?}", other ),
		}
	}

	#[ test ]
	fn writes_then_leaves_alone_on_unchanged_port()
	{
		let root = tempdir().unwrap();
		let paths = Paths::under_root( root.path() );
		fs::create_dir_all( &paths.eds_sources_dir ).unwrap();

		// First run: writes both files.
		let first = register( &paths, 12345 ).unwrap();
		match first
		{
			RegistrationOutcome::Registered { wrote, untouched } =>
			{
				assert_eq!( wrote.len(), 2 );
				assert_eq!( untouched.len(), 0 );
			}
			other => panic!( "unexpected outcome: {:?}", other ),
		}

		// Second run with the same port: both untouched.
		let second = register( &paths, 12345 ).unwrap();
		match second
		{
			RegistrationOutcome::Registered { wrote, untouched } =>
			{
				assert_eq!( wrote.len(), 0 );
				assert_eq!( untouched.len(), 2 );
			}
			other => panic!( "unexpected outcome: {:?}", other ),
		}

		// Third run with a different port: rewritten.
		let third = register( &paths, 54321 ).unwrap();
		match third
		{
			RegistrationOutcome::Registered { wrote, untouched } =>
			{
				assert_eq!( wrote.len(), 2 );
				assert_eq!( untouched.len(), 0 );
			}
			other => panic!( "unexpected outcome: {:?}", other ),
		}
	}

	#[ test ]
	fn opt_out_marker_skips_writing()
	{
		let root = tempdir().unwrap();
		let paths = Paths::under_root( root.path() );
		fs::create_dir_all( &paths.eds_sources_dir ).unwrap();
		fs::create_dir_all( paths.no_eds_autoregister.parent().unwrap() ).unwrap();
		fs::write( &paths.no_eds_autoregister, "" ).unwrap();

		match register( &paths, 12345 ).unwrap()
		{
			RegistrationOutcome::OptedOut => {}
			other => panic!( "expected OptedOut, got {:?}", other ),
		}
		// No .source files were created.
		let sources: Vec<_> = fs::read_dir( &paths.eds_sources_dir ).unwrap().collect();
		assert!( sources.is_empty() );
	}
}
