//! Real contacts provider — a D-Bus client of `org.liberux.Contacts1`.
//!
//! Mirrors the modem backend's split: the provider lives on the UI thread
//! and exposes the synchronous `ContactsProvider` trait, while a dedicated
//! worker thread runs a `smol` executor that owns the zbus connection. The
//! worker loads the full contact list once (so the first paint has data) and
//! reloads it whenever the server emits `Changed` / `BatchChanged`, keeping a
//! shared snapshot the trait methods read without ever touching the bus.

use std::collections::HashMap;
use std::sync::{ Arc, Mutex };
use std::sync::atomic::{ AtomicBool, Ordering };
use std::sync::mpsc as std_mpsc;
use std::thread;
use std::time::Duration;

use futures_util::stream::StreamExt;
use smol::LocalExecutor;
use zbus::Connection;
use zbus::zvariant::Value;

use super::{ ContactsError, ContactsProvider };
use crate::types::{ ContactRef, PhoneNumber };

const PHONE_MIME: &str = "vnd.android.cursor.item/phone_v2";
const TYPE_MOBILE: i32 = 2;

type Snapshot = Arc<Mutex<Vec<ContactRef>>>;

// Each row: ( id, display_name, photo_uri, starred, number, phone_type, phone_label ).
type SummaryRow = ( u32, String, String, bool, String, i32, String );

/// A UI-thread → worker write request.
#[ derive( Debug ) ]
enum ContactCmd
{
	Add { name: String, number: String },
}

/// What the worker's dispatch loop reacts to: a write, a server change, or a
/// dropped connection.
enum Internal
{
	Cmd( ContactCmd ),
	Reload,
	Disconnected,
}

#[ zbus::proxy(
	interface = "org.liberux.Contacts1",
	default_service = "org.liberux.Contacts1",
	default_path = "/org/liberux/Contacts1"
) ]
trait Contacts
{
	fn query_contacts_full( &self, filter: &str ) -> zbus::Result<Vec<SummaryRow>>;
	fn insert_raw_contact( &self, fields: HashMap<&str, Value<'_>> ) -> zbus::Result<u32>;
	fn add_data( &self, raw_contact_id: u32, mimetype: &str, fields: HashMap<&str, Value<'_>> ) -> zbus::Result<u32>;

	#[ zbus( signal ) ]
	fn changed( &self, contact_id: u32, change_kind: String ) -> zbus::Result<()>;

	#[ zbus( signal ) ]
	fn batch_changed( &self ) -> zbus::Result<()>;
}

pub struct DbusContacts
{
	snapshot: Snapshot,
	dirty:    Arc<AtomicBool>,
	cmd_tx:   async_channel::Sender<ContactCmd>,
	_worker:  thread::JoinHandle<()>,
}

impl DbusContacts
{
	pub fn new() -> Self
	{
		let snapshot: Snapshot = Arc::new( Mutex::new( Vec::new() ) );
		let dirty = Arc::new( AtomicBool::new( false ) );
		let snapshot_w = snapshot.clone();
		let dirty_w = dirty.clone();
		let ( cmd_tx, cmd_rx ) = async_channel::unbounded::<ContactCmd>();
		let ( ready_tx, ready_rx ) = std_mpsc::channel::<()>();

		let worker = thread::Builder::new()
			.name( "eydos-calls-contacts".into() )
			.spawn( move ||
			{
				let ex = LocalExecutor::new();
				smol::block_on( ex.run( worker_main( &ex, snapshot_w, dirty_w, cmd_rx, ready_tx ) ) );
			} )
			.expect( "failed to spawn contacts worker thread" );

		// Block briefly so the initial UI paints with the loaded contacts
		// rather than an empty list the app would never refresh.
		let _ = ready_rx.recv_timeout( Duration::from_secs( 2 ) );

		Self { snapshot, dirty, cmd_tx, _worker: worker }
	}

	fn read( &self ) -> std::sync::MutexGuard<'_, Vec<ContactRef>>
	{
		self.snapshot.lock().unwrap_or_else( |e| e.into_inner() )
	}
}

impl Default for DbusContacts
{
	fn default() -> Self
	{
		Self::new()
	}
}

impl ContactsProvider for DbusContacts
{
	fn search( &self, query: &str ) -> Result<Vec<ContactRef>, ContactsError>
	{
		let q = query.to_lowercase();
		Ok(
			self.read().iter()
				.filter( |c|
				{
					c.display_name.to_lowercase().contains( &q )
						|| c.number.as_ref().is_some_and( |n| n.as_str().contains( &q ) )
				} )
				.cloned()
				.collect()
		)
	}

	fn lookup_by_number( &self, number: &PhoneNumber ) -> Result<Option<ContactRef>, ContactsError>
	{
		let target = digits_suffix( number.as_str() );
		if target.is_empty()
		{
			return Ok( None );
		}
		Ok(
			self.read().iter()
				.find( |c| c.number.as_ref().is_some_and( |n| digits_suffix( n.as_str() ) == target ) )
				.cloned()
		)
	}

	fn all( &self ) -> Result<Vec<ContactRef>, ContactsError>
	{
		Ok( self.read().clone() )
	}

	fn add_contact( &self, name: &str, number: &str ) -> Result<(), ContactsError>
	{
		let cmd = ContactCmd::Add { name: name.to_string(), number: number.to_string() };
		self.cmd_tx
			.try_send( cmd )
			.map_err( |e| ContactsError::Backend( format!( "worker channel: {e}" ) ) )
	}

	fn poll_changed( &self ) -> bool
	{
		self.dirty.swap( false, Ordering::Relaxed )
	}
}

// ── Worker thread ────────────────────────────────────────────────

async fn worker_main(
	ex: &LocalExecutor<'_>,
	snapshot: Snapshot,
	dirty: Arc<AtomicBool>,
	cmd_rx: async_channel::Receiver<ContactCmd>,
	ready_tx: std_mpsc::Sender<()>,
)
{
	let mut signalled_ready = false;
	loop
	{
		match try_session( ex, &snapshot, &dirty, &cmd_rx, &ready_tx, &mut signalled_ready ).await
		{
			Ok( () ) => return,
			Err( e ) =>
			{
				eprintln!( "eydos-calls contacts session error: {e}" );
				if !signalled_ready
				{
					let _ = ready_tx.send( () );
					signalled_ready = true;
				}
				smol::Timer::after( Duration::from_secs( 2 ) ).await;
			}
		}
	}
}

async fn try_session(
	ex: &LocalExecutor<'_>,
	snapshot: &Snapshot,
	dirty: &Arc<AtomicBool>,
	cmd_rx: &async_channel::Receiver<ContactCmd>,
	ready_tx: &std_mpsc::Sender<()>,
	signalled_ready: &mut bool,
) -> zbus::Result<()>
{
	let conn = Connection::session().await?;
	let proxy = ContactsProxy::new( &conn ).await?;

	reload( &proxy, snapshot ).await;
	if !*signalled_ready
	{
		let _ = ready_tx.send( () );
		*signalled_ready = true;
	}

	// Fan UI writes and both change signals into one channel so a single loop
	// dispatches them. The forwarder tasks own a Connection clone and build
	// their own proxy so the signal streams outlive this borrow.
	let ( int_tx, int_rx ) = async_channel::unbounded::<Internal>();

	let _cmd_fwd = ex.spawn( {
		let cmd_rx = cmd_rx.clone();
		let int_tx = int_tx.clone();
		async move
		{
			while let Ok( cmd ) = cmd_rx.recv().await
			{
				if int_tx.send( Internal::Cmd( cmd ) ).await.is_err() { break; }
			}
		}
	} );
	let _changed_fwd = ex.spawn( {
		let conn = conn.clone();
		let int_tx = int_tx.clone();
		async move
		{
			let Ok( proxy ) = ContactsProxy::new( &conn ).await else { return; };
			if let Ok( mut s ) = proxy.receive_changed().await
			{
				while s.next().await.is_some()
				{
					if int_tx.send( Internal::Reload ).await.is_err() { return; }
				}
			}
			let _ = int_tx.send( Internal::Disconnected ).await;
		}
	} );
	let _batch_fwd = ex.spawn( {
		let conn = conn.clone();
		let int_tx = int_tx.clone();
		async move
		{
			let Ok( proxy ) = ContactsProxy::new( &conn ).await else { return; };
			if let Ok( mut s ) = proxy.receive_batch_changed().await
			{
				while s.next().await.is_some()
				{
					if int_tx.send( Internal::Reload ).await.is_err() { return; }
				}
			}
		}
	} );
	drop( int_tx );

	while let Ok( int ) = int_rx.recv().await
	{
		match int
		{
			Internal::Cmd( ContactCmd::Add { name, number } ) =>
			{
				create_contact( &proxy, &name, &number ).await;
			}
			Internal::Reload =>
			{
				reload( &proxy, snapshot ).await;
				dirty.store( true, Ordering::Relaxed );
			}
			Internal::Disconnected => break,
		}
	}
	Err( zbus::Error::Failure( "contacts session ended".into() ) )
}

async fn create_contact( proxy: &ContactsProxy<'_>, name: &str, number: &str )
{
	let mut raw: HashMap<&str, Value<'_>> = HashMap::new();
	raw.insert( "display_name", Value::from( name.to_string() ) );
	let raw_id = match proxy.insert_raw_contact( raw ).await
	{
		Ok( id ) => id,
		Err( e ) =>
		{
			eprintln!( "eydos-calls: InsertRawContact failed: {e}" );
			return;
		}
	};

	if number.is_empty()
	{
		return;
	}
	let mut phone: HashMap<&str, Value<'_>> = HashMap::new();
	phone.insert( "number", Value::from( number.to_string() ) );
	phone.insert( "type", Value::from( TYPE_MOBILE ) );
	phone.insert( "normalized_number", Value::from( normalize( number ) ) );
	if let Err( e ) = proxy.add_data( raw_id, PHONE_MIME, phone ).await
	{
		eprintln!( "eydos-calls: AddData(phone) failed: {e}" );
	}
}

/// Reduce a typed number to E.164-ish form for the server's `normalized_number`
/// column: digits only, keeping a leading `+`.
fn normalize( s: &str ) -> String
{
	let digits: String = s.chars().filter( |c| c.is_ascii_digit() ).collect();
	if s.trim_start().starts_with( '+' )
	{
		format!( "+{digits}" )
	} else {
		digits
	}
}

async fn reload( proxy: &ContactsProxy<'_>, snapshot: &Snapshot )
{
	match proxy.query_contacts_full( "" ).await
	{
		Ok( rows ) =>
		{
			let contacts: Vec<ContactRef> = rows.into_iter().map( row_to_contact ).collect();
			*snapshot.lock().unwrap_or_else( |e| e.into_inner() ) = contacts;
		}
		Err( e ) => eprintln!( "eydos-calls: QueryContactsFull failed: {e}" ),
	}
}

fn row_to_contact(
	( id, display_name, photo_uri, starred, number, phone_type, phone_label ): SummaryRow,
) -> ContactRef
{
	ContactRef
	{
		id:           id as u64,
		display_name,
		photo_uri:    ( !photo_uri.is_empty() ).then_some( photo_uri ),
		starred,
		label:        label_for( phone_type, &phone_label ),
		number:       ( !number.is_empty() ).then( || PhoneNumber::new( number ) ),
		city:         None,
	}
}

/// Map an Android `Phone.TYPE_*` value to a display label. Type `0` is the
/// custom case, where the human label rides in `custom`.
fn label_for( phone_type: i32, custom: &str ) -> Option<String>
{
	let name = match phone_type
	{
		0 => return ( !custom.is_empty() ).then( || custom.to_string() ),
		1 => "Home",
		2 => "Mobile",
		3 => "Work",
		4 => "Work Fax",
		5 => "Home Fax",
		6 => "Pager",
		7 => "Other",
		_ => return None,
	};
	Some( name.to_string() )
}

/// Last 7 digits of a number, mirroring the server's `PhoneLookup` matching.
fn digits_suffix( s: &str ) -> String
{
	let digits: String = s.chars().filter( |c| c.is_ascii_digit() ).collect();
	if digits.len() > 7
	{
		digits[ digits.len() - 7.. ].to_string()
	} else {
		digits
	}
}
