//! Implementation of `org.liberux.Contacts1`.
//!
//! Methods come in two flavours:
//!
//! - read-only (`GetRawContact`, `GetData`, `QueryData`) — wrap a single
//!   `db::*::get|query` in `smol::unblock` and shape the result.
//! - mutating (`InsertRawContact`, `AddData`, `UpdateData`, …) — perform the
//!   DB write, look up the affected `contact_id`, and emit the `Changed`
//!   signal so consumers (ATL ContentObservers, native apps) can react.
//!
//! Permission checks are intentionally absent in v1; `permission_gate` is
//! the single hook point that polkit will land on later (see TODO inside).

use std::collections::HashMap;
use std::sync::Arc;

use rusqlite::Connection;
use zbus::SignalContext;
use zbus::interface;
use zbus::zvariant::OwnedValue;

use crate::db::{ self, Database, DbError };
use crate::dbus::codec::{ dict_to_field_map, owned_to_field_value };
use crate::dbus::error::Error;
use crate::dbus::types::{ ContactDbus, ContactSummaryDbus, DataDbus, RawContactDbus };
use crate::model::contact::Contact;
use crate::eds_push::EdsPusher;
use crate::model::data_fields::{ self, FieldValue };
use crate::model::raw_contact::{ NewRawContact, RawContactPatch };

pub struct ContactsService
{
	db:       Arc<Database>,
	eds_push: Option<Arc<EdsPusher>>,
}

impl ContactsService
{
	pub fn new( db: Arc<Database>, eds_push: Option<Arc<EdsPusher>> ) -> Self
	{
		Self { db, eds_push }
	}

	/// Fire-and-forget EDS refresh. Spawns a detached task on smol so the
	/// D-Bus reply doesn't wait on the round-trip to the EDS source
	/// registry. Failures are logged at `debug` inside `EdsPusher::refresh`.
	fn schedule_eds_refresh( &self )
	{
		if let Some( pusher ) = &self.eds_push
		{
			let pusher = pusher.clone();
			smol::spawn( async move { pusher.refresh().await; } ).detach();
		}
	}

	/// Emit the `Changed` D-Bus signal for native subscribers and ping EDS
	/// so gnome-contacts / Evolution refresh in seconds rather than after
	/// their next poll.
	async fn notify_change(
		&self,
		ctxt: &SignalContext<'_>,
		contact_id: i64,
		kind: &str,
	) -> Result<(), Error>
	{
		Self::changed( ctxt, contact_id as u32, kind.to_owned() )
			.await
			.map_err( |e| Error::Internal( format!( "emit Changed signal: {e}" ) ) )?;
		self.schedule_eds_refresh();
		Ok( () )
	}

	/// Run a synchronous DB closure on the blocking thread pool. The closure
	/// receives a `&mut Connection`, runs to completion, and returns its
	/// result back to the async caller.
	async fn run_db<F, R>( &self, f: F ) -> Result<R, Error>
	where
		F: FnOnce( &mut Connection ) -> Result<R, DbError> + Send + 'static,
		R: Send + 'static,
	{
		let database = self.db.clone();
		let outcome = smol::unblock( move || database.with_conn( f ) ).await;
		outcome.map_err( Error::from )
	}

	/// Permission check hook. v1 always allows; future polkit integration
	/// goes here so call sites stay untouched.
	fn permission_gate( &self, _method: &'static str ) -> Result<(), Error>
	{
		// TODO( permissions ): wire polkit here once ATL surfaces a per-app
		// sender identity. Until then every session-bus caller is allowed.
		Ok( () )
	}
}

#[ interface( name = "org.liberux.Contacts1" ) ]
impl ContactsService
{
	// -- RawContacts -------------------------------------------------------

	async fn get_raw_contact( &self, id: u32 ) -> Result<RawContactDbus, Error>
	{
		self.permission_gate( "GetRawContact" )?;
		let i = id as i64;
		let raw = self.run_db( move |c| db::raw_contacts::get( c, i ) ).await?;
		Ok( raw.into() )
	}

	async fn insert_raw_contact(
		&self,
		fields: HashMap<String, OwnedValue>,
		#[ zbus( signal_context ) ] ctxt: SignalContext<'_>,
	) -> Result<u32, Error>
	{
		self.permission_gate( "InsertRawContact" )?;
		let new = decode_new_raw_contact( fields )?;
		let ( raw_id, contact_id ) =
			self.run_db( move |c| db::raw_contacts::insert( c, new ) ).await?;
		self.notify_change( &ctxt, contact_id, "added" ).await?;
		Ok( raw_id as u32 )
	}

	async fn update_raw_contact(
		&self,
		id: u32,
		patch: HashMap<String, OwnedValue>,
		#[ zbus( signal_context ) ] ctxt: SignalContext<'_>,
	) -> Result<(), Error>
	{
		self.permission_gate( "UpdateRawContact" )?;
		let i = id as i64;
		let p = decode_raw_contact_patch( patch )?;
		let contact_id = self.run_db( move |c|
		{
			db::raw_contacts::update( c, i, p )?;
			db::raw_contacts::contact_id_of( c, i )
		} ).await?;
		self.notify_change( &ctxt, contact_id, "updated" ).await?;
		Ok( () )
	}

	async fn delete_raw_contact(
		&self,
		id: u32,
		#[ zbus( signal_context ) ] ctxt: SignalContext<'_>,
	) -> Result<(), Error>
	{
		self.permission_gate( "DeleteRawContact" )?;
		let i = id as i64;
		let contact_id = self.run_db( move |c|
		{
			let cid = db::raw_contacts::contact_id_of( c, i )?;
			db::raw_contacts::soft_delete( c, i )?;
			Ok::<i64, DbError>( cid )
		} ).await?;
		self.notify_change( &ctxt, contact_id, "deleted" ).await?;
		Ok( () )
	}

	// -- Data --------------------------------------------------------------

	async fn get_data( &self, id: u32 ) -> Result<DataDbus, Error>
	{
		self.permission_gate( "GetData" )?;
		let i = id as i64;
		let row = self.run_db( move |c| db::data::get( c, i ) ).await?;
		Ok( DataDbus::from_row( row )? )
	}

	async fn add_data(
		&self,
		raw_contact_id: u32,
		mimetype: String,
		fields: HashMap<String, OwnedValue>,
		#[ zbus( signal_context ) ] ctxt: SignalContext<'_>,
	) -> Result<u32, Error>
	{
		self.permission_gate( "AddData" )?;
		let raw_id = raw_contact_id as i64;
		let fmap = dict_to_field_map( fields )?;
		let new = data_fields::new_data_row_from_fields( raw_id, &mimetype, fmap )?;
		let ( data_id, contact_id ) = self.run_db( move |c|
		{
			let did = db::data::insert( c, new )?;
			let cid = db::raw_contacts::contact_id_of( c, raw_id )?;
			Ok::<( i64, i64 ), DbError>( ( did, cid ) )
		} ).await?;
		self.notify_change( &ctxt, contact_id, "updated" ).await?;
		Ok( data_id as u32 )
	}

	async fn update_data(
		&self,
		id: u32,
		patch: HashMap<String, OwnedValue>,
		#[ zbus( signal_context ) ] ctxt: SignalContext<'_>,
	) -> Result<(), Error>
	{
		self.permission_gate( "UpdateData" )?;
		let i = id as i64;
		// Mimetype is fixed for the lifetime of a data row, so we can read
		// it independently and validate the patch keys outside the write
		// transaction without risking a TOCTOU on the type itself.
		let mimetype = self.run_db( move |c| db::data::mimetype_of( c, i ) ).await?;
		let fmap = dict_to_field_map( patch )?;
		let p = data_fields::data_patch_from_fields( &mimetype, fmap )?;

		let contact_id = self.run_db( move |c|
		{
			db::data::update( c, i, p )?;
			let raw_id = db::data::raw_contact_id_of( c, i )?;
			db::raw_contacts::contact_id_of( c, raw_id )
		} ).await?;
		self.notify_change( &ctxt, contact_id, "updated" ).await?;
		Ok( () )
	}

	async fn delete_data(
		&self,
		id: u32,
		#[ zbus( signal_context ) ] ctxt: SignalContext<'_>,
	) -> Result<(), Error>
	{
		self.permission_gate( "DeleteData" )?;
		let i = id as i64;
		let contact_id = self.run_db( move |c|
		{
			let raw_id = db::data::raw_contact_id_of( c, i )?;
			let cid    = db::raw_contacts::contact_id_of( c, raw_id )?;
			db::data::delete( c, i )?;
			Ok::<i64, DbError>( cid )
		} ).await?;
		self.notify_change( &ctxt, contact_id, "updated" ).await?;
		Ok( () )
	}

	async fn query_data(
		&self,
		raw_contact_id: u32,
		mimetype: String,
	) -> Result<Vec<DataDbus>, Error>
	{
		self.permission_gate( "QueryData" )?;
		let raw_id = raw_contact_id as i64;
		let mt_filter = if mimetype.is_empty() { None } else { Some( mimetype ) };
		let rows = self.run_db( move |c|
			db::data::query( c, raw_id, mt_filter.as_deref() )
		).await?;
		rows.into_iter()
			.map( |r| DataDbus::from_row( r ).map_err( Error::from ) )
			.collect()
	}

	// -- Contacts (aggregated) --------------------------------------------

	async fn get_contact( &self, id: u32 ) -> Result<ContactDbus, Error>
	{
		self.permission_gate( "GetContact" )?;
		let i = id as i64;
		let c = self.run_db( move |c| db::contacts::get( c, i ) ).await?;
		Ok( c.into() )
	}

	/// Returns `( contact_id, display_name, photo_uri )` triples. Empty
	/// `filter` returns every non-deleted contact, sorted by display name.
	/// `photo_uri` is the empty string if the contact has no photo;
	/// otherwise `liberux-contacts:///data/<data_id>` so a future
	/// `GetPhoto` D-Bus method (or a direct `GetData`) can fetch the
	/// bytes — clients should treat the URI as opaque.
	async fn query_contacts(
		&self,
		filter: String,
	) -> Result<Vec<( u32, String, String )>, Error>
	{
		self.permission_gate( "QueryContacts" )?;
		let contacts = self.run_db( move |c| db::contacts::list( c, &filter ) ).await?;
		Ok( contacts.into_iter().map( contact_to_query_triple ).collect() )
	}

	/// Like [`Self::query_contacts`], but each entry also carries the
	/// contact's primary phone number, Android phone `type`, and custom
	/// label — saving list views a `QueryData` round-trip per contact.
	async fn query_contacts_full(
		&self,
		filter: String,
	) -> Result<Vec<ContactSummaryDbus>, Error>
	{
		self.permission_gate( "QueryContactsFull" )?;
		let rows = self.run_db( move |c| db::contacts::list_full( c, &filter ) ).await?;
		Ok( rows.into_iter().map( ContactSummaryDbus::from ).collect() )
	}

	/// Returns the `raw_contacts._id` of every raw_contact owning a
	/// `…/phone_v2` data row whose `data4` (E.164-normalised number)
	/// ends with the last 7 digits of `number`. Mirrors Android's
	/// `PhoneLookup` semantics.
	async fn search_by_phone_number(
		&self,
		number: String,
	) -> Result<Vec<u32>, Error>
	{
		self.permission_gate( "SearchByPhoneNumber" )?;
		let ids = self.run_db( move |c|
			db::data::search_by_normalized_number( c, &number )
		).await?;
		Ok( ids.into_iter().map( |id| id as u32 ).collect() )
	}

	// -- ApplyBatch --------------------------------------------------------

	async fn apply_batch(
		&self,
		ops: Vec<crate::dbus::batch::BatchOp>,
		#[ zbus( signal_context ) ] ctxt: SignalContext<'_>,
	) -> Result<Vec<crate::dbus::batch::BatchResult>, Error>
	{
		self.permission_gate( "ApplyBatch" )?;

		let database = self.db.clone();
		let results = smol::unblock( move ||
		{
			database.with_conn( |conn| crate::dbus::batch::apply( conn, ops ) )
		} ).await?;

		Self::batch_changed( &ctxt )
			.await
			.map_err( |e| Error::Internal( format!( "emit BatchChanged: {e}" ) ) )?;
		self.schedule_eds_refresh();

		Ok( results )
	}

	// -- Signals -----------------------------------------------------------

	#[ zbus( signal ) ]
	async fn changed(
		ctxt: &SignalContext<'_>,
		contact_id: u32,
		change_kind: String,
	) -> zbus::Result<()>;

	#[ zbus( signal ) ]
	async fn batch_changed( ctxt: &SignalContext<'_> ) -> zbus::Result<()>;
}

fn contact_to_query_triple( c: Contact ) -> ( u32, String, String )
{
	let photo_uri = match c.photo_id
	{
		Some( pid ) if pid > 0 => format!( "liberux-contacts:///data/{}", pid ),
		_                      => String::new(),
	};
	( c.id as u32, c.display_name.unwrap_or_default(), photo_uri )
}

fn decode_new_raw_contact(
	dict: HashMap<String, OwnedValue>,
) -> Result<NewRawContact, Error>
{
	let mut new = NewRawContact::default();
	for ( key, value ) in dict
	{
		let s = extract_string( &value, &key )?;
		match key.as_str()
		{
			"account_name" => new.account_name = Some( s ),
			"account_type" => new.account_type = Some( s ),
			"display_name" => new.display_name = Some( s ),
			"sourceid"     => new.sourceid     = Some( s ),
			// Forward compatibility: drop unknown keys silently so server/
			// client version skews don't trip on extra metadata.
			_              => {}
		}
	}
	if new.account_type.is_none()
	{
		new.account_type = Some( "org.liberux.local".to_owned() );
	}
	Ok( new )
}

fn decode_raw_contact_patch(
	dict: HashMap<String, OwnedValue>,
) -> Result<RawContactPatch, Error>
{
	let mut patch = RawContactPatch::default();
	for ( key, value ) in dict
	{
		let s = extract_string( &value, &key )?;
		match key.as_str()
		{
			"account_name" => patch.account_name = Some( Some( s ) ),
			"account_type" => patch.account_type = Some( Some( s ) ),
			"display_name" => patch.display_name = Some( Some( s ) ),
			"sourceid"     => patch.sourceid     = Some( Some( s ) ),
			_              => {}
		}
	}
	Ok( patch )
}

fn extract_string( v: &OwnedValue, key: &str ) -> Result<String, Error>
{
	match owned_to_field_value( v )?
	{
		FieldValue::Text( s ) => Ok( s ),
		other => Err( Error::InvalidArgument(
			format!( "key {:?}: expected string, got {:?}", key, other.ty() ),
		) ),
	}
}
