//! Implementation of `org.liberux.CallLog1`.
//!
//! Read methods wrap a single `db::calls::*` call in `smol::unblock`; mutating
//! methods do the write and then emit the parameterless `Changed` signal so
//! consumers (the eydos-calls client, ATL ContentObservers) refresh.
//!
//! Permission checks are intentionally absent in v1; `permission_gate` is the
//! single hook point polkit will land on later.

use std::sync::Arc;

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

use crate::db::{ self, Database, DbError };
use crate::dbus::error::Error;
use crate::dbus::types::CallDbus;
use crate::model::NewCall;

pub struct CallLogService
{
	db: Arc<Database>,
}

impl CallLogService
{
	pub fn new( db: Arc<Database> ) -> Self
	{
		Self { db }
	}

	/// Run a synchronous DB closure on the blocking thread pool.
	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();
		smol::unblock( move || database.with_conn( f ) ).await.map_err( Error::from )
	}

	async fn notify_change( ctxt: &SignalContext<'_> ) -> Result<(), Error>
	{
		Self::changed( ctxt )
			.await
			.map_err( |e| Error::Internal( format!( "emit Changed signal: {e}" ) ) )
	}

	/// 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.CallLog1" ) ]
impl CallLogService
{
	/// Record a finished call. `cached_name` may be empty for unknown callers.
	/// Returns the new `calls._id`. Emits `Changed`.
	async fn add_call(
		&self,
		number: String,
		date: i64,
		duration: i64,
		call_type: i32,
		cached_name: String,
		#[ zbus( signal_context ) ] ctxt: SignalContext<'_>,
	) -> Result<u32, Error>
	{
		self.permission_gate( "AddCall" )?;
		let new = NewCall
		{
			number,
			date,
			duration,
			call_type,
			cached_name: ( !cached_name.is_empty() ).then_some( cached_name ),
		};
		let id = self.run_db( move |c| db::calls::insert( c, new ) ).await?;
		Self::notify_change( &ctxt ).await?;
		Ok( id as u32 )
	}

	/// Most recent calls, newest-first. `limit = 0` returns every row.
	async fn query_recent( &self, limit: u32 ) -> Result<Vec<CallDbus>, Error>
	{
		self.permission_gate( "QueryRecent" )?;
		let rows = self.run_db( move |c| db::calls::recent( c, limit as i64 ) ).await?;
		Ok( rows.into_iter().map( CallDbus::from ).collect() )
	}

	async fn get_call( &self, id: u32 ) -> Result<CallDbus, Error>
	{
		self.permission_gate( "GetCall" )?;
		let i = id as i64;
		let row = self.run_db( move |c| db::calls::get( c, i ) ).await?;
		Ok( CallDbus::from( row ) )
	}

	/// Delete a single call by id. Errors `NotFound` if absent. Emits `Changed`.
	async fn delete_call(
		&self,
		id: u32,
		#[ zbus( signal_context ) ] ctxt: SignalContext<'_>,
	) -> Result<(), Error>
	{
		self.permission_gate( "DeleteCall" )?;
		let i = id as i64;
		self.run_db( move |c| db::calls::delete( c, i ) ).await?;
		Self::notify_change( &ctxt ).await?;
		Ok( () )
	}

	/// Clear the whole log. Emits `Changed`.
	async fn delete_all(
		&self,
		#[ zbus( signal_context ) ] ctxt: SignalContext<'_>,
	) -> Result<(), Error>
	{
		self.permission_gate( "DeleteAll" )?;
		self.run_db( |c| db::calls::delete_all( c ) ).await?;
		Self::notify_change( &ctxt ).await?;
		Ok( () )
	}

	/// Clear the unread (`new`) flag on every call. Emits `Changed`.
	async fn mark_all_read(
		&self,
		#[ zbus( signal_context ) ] ctxt: SignalContext<'_>,
	) -> Result<(), Error>
	{
		self.permission_gate( "MarkAllRead" )?;
		self.run_db( |c| db::calls::mark_all_read( c ) ).await?;
		Self::notify_change( &ctxt ).await?;
		Ok( () )
	}

	/// Fired after any mutation. Carries no payload — observers refresh
	/// whatever query they are watching.
	#[ zbus( signal ) ]
	async fn changed( ctxt: &SignalContext<'_> ) -> zbus::Result<()>;
}
