//! Real call-log provider — a D-Bus client of `org.liberux.CallLog1`.
//!
//! Same split as the modem backend and `contacts::dbus`: the provider lives
//! on the UI thread behind the synchronous `CallLog` trait while a worker
//! thread owns the zbus connection. `record` is fire-and-forget over a
//! command channel; `recent` reads a shared snapshot the worker fills from
//! `QueryRecent` and reloads on every `Changed` signal (which the server emits
//! after our own `AddCall` too, so recorded calls round-trip back in).

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 super::{ CallLog, HistoryError };
use crate::types::{ CallDirection, EndReason, HistoryEntry, PhoneNumber };

/// How many rows to keep in the snapshot. The UI asks for far fewer.
const SNAPSHOT_LIMIT: u32 = 200;

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

// Each row: ( id, number, date, duration, type, new, cached_name ).
type CallRow = ( u32, String, i64, i64, i32, bool, String );

#[ derive( Debug ) ]
struct NewCallCmd
{
	number:      String,
	date:        i64,
	duration:    i64,
	call_type:   i32,
	cached_name: String,
}

#[ derive( Debug ) ]
enum Internal
{
	Add( NewCallCmd ),
	Reload,
	Disconnected,
}

#[ zbus::proxy(
	interface = "org.liberux.CallLog1",
	default_service = "org.liberux.CallLog1",
	default_path = "/org/liberux/CallLog1"
) ]
trait CallLogServer
{
	fn add_call( &self, number: &str, date: i64, duration: i64, call_type: i32, cached_name: &str ) -> zbus::Result<u32>;
	fn query_recent( &self, limit: u32 ) -> zbus::Result<Vec<CallRow>>;

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

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

impl DbusCallLog
{
	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::<NewCallCmd>();
		let ( ready_tx, ready_rx ) = std_mpsc::channel::<()>();

		let worker = thread::Builder::new()
			.name( "eydos-calls-calllog".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 calllog worker thread" );

		// Block briefly so the first paint has recents rather than an empty
		// list the app would only refresh on the next change.
		let _ = ready_rx.recv_timeout( Duration::from_secs( 2 ) );

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

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

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

impl CallLog for DbusCallLog
{
	fn record( &mut self, entry: HistoryEntry ) -> Result<(), HistoryError>
	{
		let cmd = NewCallCmd
		{
			number:      entry.number.as_str().to_string(),
			date:        entry.started_at_ms,
			duration:    entry.duration.as_secs() as i64,
			call_type:   call_type_of( entry.direction, entry.end ),
			cached_name: entry.contact.as_ref().map( |c| c.display_name.clone() ).unwrap_or_default(),
		};
		self.cmd_tx
			.try_send( cmd )
			.map_err( |e| HistoryError::Backend( format!( "worker channel: {e}" ) ) )
	}

	fn recent( &self, limit: usize ) -> Result<Vec<HistoryEntry>, HistoryError>
	{
		Ok( self.read().iter().take( limit ).cloned().collect() )
	}

	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<NewCallCmd>,
	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 calllog 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<NewCallCmd>,
	ready_tx: &std_mpsc::Sender<()>,
	signalled_ready: &mut bool,
) -> zbus::Result<()>
{
	let conn = Connection::session().await?;
	let proxy = CallLogServerProxy::new( &conn ).await?;

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

	// Fan UI commands and the Changed signal into one channel so a single loop
	// dispatches both. The forwarder tasks own what they need so their 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::Add( cmd ) ).await.is_err() { break; }
			}
		}
	} );
	let _sig_fwd = ex.spawn( {
		let conn = conn.clone();
		let int_tx = int_tx.clone();
		async move
		{
			let Ok( proxy ) = CallLogServerProxy::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; }
				}
			}
			// Stream ended — the connection dropped. Ask the loop to reconnect.
			let _ = int_tx.send( Internal::Disconnected ).await;
		}
	} );
	drop( int_tx );

	while let Ok( int ) = int_rx.recv().await
	{
		match int
		{
			Internal::Add( cmd ) =>
			{
				if let Err( e ) = proxy.add_call(
					&cmd.number, cmd.date, cmd.duration, cmd.call_type, &cmd.cached_name,
				).await
				{
					eprintln!( "eydos-calls: CallLog.AddCall failed: {e}" );
				}
			}
			Internal::Reload =>
			{
				reload( &proxy, snapshot ).await;
				dirty.store( true, Ordering::Relaxed );
			}
			Internal::Disconnected => break,
		}
	}
	Err( zbus::Error::Failure( "calllog session ended".into() ) )
}

async fn reload( proxy: &CallLogServerProxy<'_>, snapshot: &Snapshot )
{
	match proxy.query_recent( SNAPSHOT_LIMIT ).await
	{
		Ok( rows ) =>
		{
			let entries: Vec<HistoryEntry> = rows.into_iter().map( row_to_entry ).collect();
			*snapshot.lock().unwrap_or_else( |e| e.into_inner() ) = entries;
		}
		Err( e ) => eprintln!( "eydos-calls: QueryRecent failed: {e}" ),
	}
}

fn row_to_entry(
	( _id, number, date, duration, call_type, _new, _cached_name ): CallRow,
) -> HistoryEntry
{
	let ( direction, end ) = decode_type( call_type );
	HistoryEntry
	{
		number:        PhoneNumber::new( number ),
		contact:       None,
		direction,
		end,
		started_at_ms: date,
		duration:      Duration::from_secs( duration.max( 0 ) as u64 ),
	}
}

/// Map our (direction, outcome) to an Android `CallLog.Calls.TYPE`.
fn call_type_of( direction: CallDirection, end: EndReason ) -> i32
{
	use crate::types::CallDirection::*;
	match ( direction, end )
	{
		( Incoming, EndReason::Missed )   => 3,
		( Incoming, EndReason::Rejected ) => 5,
		( Incoming, _ )                   => 1,
		( Outgoing, _ )                   => 2,
	}
}

/// Reconstruct a (direction, outcome) pair from an Android type. We only need
/// enough fidelity for `HistoryEntry::kind()` (missed vs in/out); the original
/// hangup reason is not stored.
fn decode_type( call_type: i32 ) -> ( CallDirection, EndReason )
{
	match call_type
	{
		2 => ( CallDirection::Outgoing, EndReason::RemoteHangup ),
		3 => ( CallDirection::Incoming, EndReason::Missed ),
		5 => ( CallDirection::Incoming, EndReason::Rejected ),
		_ => ( CallDirection::Incoming, EndReason::RemoteHangup ),
	}
}
