//! Application shell — owns state, dispatches messages, wires the
//! backends (ModemManager, PipeWire, contacts-server, calllogs-server)
//! to the LTK runtime behind their traits.

use std::time::{ Duration, Instant };

use ltk::{ App, Element, Keysym, OverlaySpec };

use crate::audio::CallAudio;
use crate::contacts::ContactsProvider;
use crate::history::CallLog;
use crate::ringtone::Ringtone;
use crate::modem::{ CallBackend, CallEvent };
use crate::types::
{
	Call, CallDirection, CallId, CallState, ContactRef, EndReason,
	HistoryEntry, PhoneNumber,
};
use crate::views::{ create, dialer, home, incall, incoming };

#[ derive( Clone, Copy, Debug, Eq, PartialEq ) ]
pub enum HomeTab
{
	Calls,
	Contacts,
}

#[ derive( Clone, Debug ) ]
pub enum Message
{
	// Dialpad
	DialerDigit( char ),
	DialerBackspace,
	DialerCall,
	CallNumber( PhoneNumber ),

	// Incoming-call screen
	Accept,
	Reject,

	// In-call screen
	Hangup,
	ToggleMute,
	ToggleSpeaker,
	ToggleDtmfPad,
	DtmfDigit( char ),

	// Navigation
	SelectHomeTab( HomeTab ),
	OpenDialpad,
	CloseDialpad,

	// History / contacts
	RedialFromHistory( PhoneNumber ),
	OpenContact( u64 ),
	ContactsChanged,
	HistoryChanged,

	// Create-contact screen
	OpenCreateContact,
	CloseCreateContact,
	CreateNameChanged( String ),
	CreateNumberChanged( String ),
	SaveContact,

	// Internal — produced by `poll_external` from backend events.
	Backend( CallEvent ),
}

pub struct CallsApp
{
	backend:  Box<dyn CallBackend>,
	audio:    Box<dyn CallAudio>,
	contacts: Box<dyn ContactsProvider>,
	history:  Box<dyn CallLog>,
	ringtone: Ringtone,

	home_tab:       HomeTab,
	dialpad_open:   bool,
	dialer_number:  String,
	active_call:    Option<Call>,
	incoming_call:  Option<Call>,
	dtmf_pad_open:  bool,

	create_open:    bool,
	create_name:    String,
	create_number:  String,

	contacts_cache: Vec<ContactRef>,
	history_cache:  Vec<HistoryEntry>,

	window_size:    Option<( u32, u32 )>,
}

impl CallsApp
{
	pub fn new(
		backend:     Box<dyn CallBackend>,
		audio:       Box<dyn CallAudio>,
		contacts:    Box<dyn ContactsProvider>,
		history:     Box<dyn CallLog>,
		window_size: Option<( u32, u32 )>,
	) -> Self
	{
		let contacts_cache = contacts.all().unwrap_or_default();
		let mut history_cache = history.recent( 50 ).unwrap_or_default();
		resolve_contacts( contacts.as_ref(), &mut history_cache );

		Self
		{
			backend,
			audio,
			contacts,
			history,
			ringtone:       Ringtone::new(),

			home_tab:       HomeTab::Calls,
			dialpad_open:   false,
			dialer_number:  String::new(),
			active_call:    None,
			incoming_call:  None,
			dtmf_pad_open:  false,

			create_open:    false,
			create_name:    String::new(),
			create_number:  String::new(),

			contacts_cache,
			history_cache,

			window_size,
		}
	}

	fn refresh_history_cache( &mut self )
	{
		let mut entries = self.history.recent( 50 ).unwrap_or_default();
		resolve_contacts( self.contacts.as_ref(), &mut entries );
		self.history_cache = entries;
	}

	fn refresh_contacts_cache( &mut self )
	{
		self.contacts_cache = self.contacts.all().unwrap_or_default();
	}

	fn lookup_contact( &self, number: &PhoneNumber ) -> Option<ContactRef>
	{
		self.contacts.lookup_by_number( number ).ok().flatten()
	}

	fn place_call( &mut self, number: PhoneNumber )
	{
		if number.as_str().is_empty() { return; }
		if let Ok( id ) = self.backend.dial( &number )
		{
			self.active_call = Some( Call
			{
				id,
				number,
				direction: CallDirection::Outgoing,
				state:     CallState::Dialing,
				since:     Instant::now(),
				talk_time: Duration::ZERO,
				muted:     false,
				speaker:   false,
			} );
			self.dialer_number.clear();
			self.dialpad_open = false;
		}
	}

	fn record_history( &mut self, call: &Call, end: EndReason )
	{
		let started_at_ms = unix_ms_now() - call.talk_time.as_millis() as i64;
		let entry = HistoryEntry
		{
			number:        call.number.clone(),
			contact:       self.lookup_contact( &call.number ),
			direction:     call.direction,
			end,
			started_at_ms,
			duration:      call.talk_time,
		};
		let _ = self.history.record( entry );
		self.refresh_history_cache();
	}

	fn handle_backend( &mut self, ev: CallEvent )
	{
		match ev
		{
			CallEvent::Incoming { id, number } =>
			{
				self.incoming_call = Some( Call
				{
					id,
					number,
					direction: CallDirection::Incoming,
					state:     CallState::Ringing,
					since:     Instant::now(),
					talk_time: Duration::ZERO,
					muted:     false,
					speaker:   false,
				} );
				self.ringtone.start();
			}

			CallEvent::StateChanged { id, state } =>
			{
				self.handle_state_change( id, state );
			}

			CallEvent::BackendUnavailable | CallEvent::BackendAvailable =>
			{
				// TODO surface a banner once we have one.
			}
		}
	}

	fn handle_state_change( &mut self, id: CallId, state: CallState )
	{
		if self.incoming_call.as_ref().is_some_and( |c| c.id == id )
		{
			self.handle_incoming_state_change( state );
		} else if self.active_call.as_ref().is_some_and( |c| c.id == id ) {
			self.handle_active_state_change( id, state );
		}
	}

	fn handle_incoming_state_change( &mut self, state: CallState )
	{
		// The call is no longer ringing once it is answered or it ends.
		if !matches!( state, CallState::Ringing )
		{
			self.ringtone.stop();
		}

		match state
		{
			CallState::Accepting | CallState::Active =>
			{
				let mut promoted = self.incoming_call.take().unwrap();
				promoted.state = state;
				if matches!( state, CallState::Active )
				{
					promoted.since = Instant::now();
					let _ = self.audio.begin_call( promoted.id );
				}
				self.active_call = Some( promoted );
			}

			CallState::Ended( reason ) =>
			{
				let ended = self.incoming_call.take().unwrap();
				let final_reason = match reason
				{
					EndReason::LocalHangup | EndReason::RemoteHangup => EndReason::Missed,
					other                                            => other,
				};
				self.record_history( &ended, final_reason );
			}

			other =>
			{
				if let Some( call ) = self.incoming_call.as_mut()
				{
					call.state = other;
				}
			}
		}
	}

	fn handle_active_state_change( &mut self, id: CallId, state: CallState )
	{
		match state
		{
			CallState::Active =>
			{
				if let Some( call ) = self.active_call.as_mut()
				{
					let was_active = matches!( call.state, CallState::Active );
					call.state = state;
					if !was_active
					{
						call.since = Instant::now();
					}
				}
				let _ = self.audio.begin_call( id );
			}

			CallState::Ended( reason ) =>
			{
				if let Some( call ) = self.active_call.as_mut()
				{
					if matches!( call.state, CallState::Active )
					{
						call.talk_time += call.since.elapsed();
					}
				}
				let _ = self.audio.end_call( id );
				let ended = self.active_call.take().unwrap();
				self.record_history( &ended, reason );
				self.dtmf_pad_open = false;
			}

			other =>
			{
				if let Some( call ) = self.active_call.as_mut()
				{
					call.state = other;
				}
			}
		}
	}
}


impl App for CallsApp
{
	type Message = Message;

	fn app_id( &self ) -> &str { "io.eydos.Calls" }

	fn save_state( &self ) -> Option<Vec<u8>>
	{
		let tab = match self.home_tab { HomeTab::Calls => "calls", HomeTab::Contacts => "contacts" };
		Some( format!( "1\n{tab}\n{}\n{}", self.dialpad_open as u8, self.dialer_number ).into_bytes() )
	}

	fn restore_state( &mut self, state: Vec<u8> )
	{
		let Ok( text ) = String::from_utf8( state ) else { return };
		let mut lines = text.splitn( 4, '\n' );
		if lines.next() != Some( "1" ) { return; }
		match lines.next()
		{
			Some( "calls" )    => self.home_tab = HomeTab::Calls,
			Some( "contacts" ) => self.home_tab = HomeTab::Contacts,
			_ => {}
		}
		if let Some( open ) = lines.next() { self.dialpad_open = open == "1"; }
		if let Some( number ) = lines.next() { self.dialer_number = number.to_string(); }
	}

	fn view( &self ) -> Element<Message>
	{
		// Modal screens take precedence over the tabbed UI.
		if let Some( call ) = &self.incoming_call
		{
			let contact = self.lookup_contact( &call.number );
			return incoming::view( call, contact.as_ref() );
		}

		if let Some( call ) = &self.active_call
		{
			let contact = self.lookup_contact( &call.number );
			return incall::view( call, contact.as_ref(), self.dtmf_pad_open );
		}

		if self.dialpad_open
		{
			return dialer::view( &self.dialer_number, &self.contacts_cache );
		}

		if self.create_open
		{
			return create::view( &self.create_name, &self.create_number );
		}

		let favorites: Vec<ContactRef> = self.contacts_cache.iter()
			.filter( |c| c.starred )
			.cloned()
			.collect();

		home::view(
			self.home_tab,
			&favorites,
			&self.history_cache,
			&self.contacts_cache,
			unix_ms_now(),
		)
	}

	fn update( &mut self, msg: Message )
	{
		match msg
		{
			Message::DialerDigit( d ) =>
			{
				self.dialer_number.push( d );
			}

			Message::DialerBackspace =>
			{
				self.dialer_number.pop();
			}

			Message::DialerCall =>
			{
				self.place_call( PhoneNumber::new( self.dialer_number.clone() ) );
			}

			Message::CallNumber( number ) =>
			{
				self.place_call( number );
			}

			Message::Accept =>
			{
				if let Some( call ) = &self.incoming_call
				{
					self.ringtone.stop();
					let _ = self.backend.accept( call.id );
				}
			}

			Message::Reject =>
			{
				if let Some( call ) = &self.incoming_call
				{
					self.ringtone.stop();
					let _ = self.backend.reject( call.id );
				}
			}

			Message::Hangup =>
			{
				if let Some( call ) = &self.active_call
				{
					let _ = self.backend.hangup( call.id );
				}
			}

			Message::ToggleMute =>
			{
				if let Some( call ) = self.active_call.as_mut()
				{
					call.muted = !call.muted;
					let _ = self.backend.set_mute( call.id, call.muted );
					let _ = self.audio.set_mute( call.id, call.muted );
				}
			}

			Message::ToggleSpeaker =>
			{
				if let Some( call ) = self.active_call.as_mut()
				{
					call.speaker = !call.speaker;
					let _ = self.audio.set_speaker( call.id, call.speaker );
				}
			}

			Message::ToggleDtmfPad =>
			{
				self.dtmf_pad_open = !self.dtmf_pad_open;
			}

			Message::DtmfDigit( d ) =>
			{
				if let Some( call ) = &self.active_call
				{
					let _ = self.backend.send_dtmf( call.id, d );
				}
			}

			Message::SelectHomeTab( tab ) =>
			{
				self.home_tab = tab;
			}

			Message::OpenDialpad =>
			{
				self.dialpad_open = true;
			}

			Message::CloseDialpad =>
			{
				self.dialpad_open = false;
			}

			Message::RedialFromHistory( number ) =>
			{
				self.dialer_number = number.as_str().to_string();
				self.dialpad_open  = true;
			}

			Message::OpenContact( _id ) =>
			{
				// TODO contact detail view.
			}

			Message::ContactsChanged =>
			{
				self.refresh_contacts_cache();
			}

			Message::HistoryChanged =>
			{
				self.refresh_history_cache();
			}

			Message::OpenCreateContact =>
			{
				self.create_name.clear();
				self.create_number.clear();
				self.create_open = true;
			}

			Message::CloseCreateContact =>
			{
				self.create_open = false;
			}

			Message::CreateNameChanged( v ) =>
			{
				self.create_name = v;
			}

			Message::CreateNumberChanged( v ) =>
			{
				self.create_number = v;
			}

			Message::SaveContact =>
			{
				let name = self.create_name.trim();
				let number = self.create_number.trim();
				if !name.is_empty() || !number.is_empty()
				{
					let _ = self.contacts.add_contact( name, number );
				}
				self.create_open = false;
			}

			Message::Backend( ev ) =>
			{
				self.handle_backend( ev );
			}
		}
	}

	fn overlays( &self ) -> Vec<OverlaySpec<Message>>
	{
		Vec::new()
	}

	fn on_key( &mut self, keysym: Keysym ) -> Option<Message>
	{
		if keysym == Keysym::Escape
		{
			if self.create_open
			{
				return Some( Message::CloseCreateContact );
			}
			if self.dialpad_open
			{
				return Some( Message::CloseDialpad );
			}
			std::process::exit( 0 );
		}
		None
	}

	fn is_animating( &self ) -> bool
	{
		// Active call needs a steady redraw to update the timer; an
		// incoming ringer benefits too if we ever animate the avatar.
		self.active_call.is_some() || self.incoming_call.is_some()
	}

	fn poll_interval( &self ) -> Option<Duration>
	{
		// Constant cadence to drain backend events and pick up contact /
		// call-log changes regardless of UI animation needs.
		Some( Duration::from_millis( 200 ) )
	}

	fn poll_external( &mut self ) -> Vec<Message>
	{
		let mut msgs: Vec<Message> = self.backend.poll_events()
			.into_iter()
			.map( Message::Backend )
			.collect();
		if self.contacts.poll_changed()
		{
			msgs.push( Message::ContactsChanged );
		}
		if self.history.poll_changed()
		{
			msgs.push( Message::HistoryChanged );
		}
		msgs
	}

	// ── Window geometry ─────────────────────────────────────────────

	fn window_size_hint( &self ) -> Option<( u32, u32 )>
	{
		self.window_size
	}

	fn start_fullscreen( &self ) -> bool
	{
		self.window_size.is_none()
	}

	fn window_config( &self ) -> Option<( &str, &str )>
	{
		match self.window_size
		{
			Some( _ ) => Some( ( "Eydos Calls", "io.eydos.Calls" ) ),
			None      => None,
		}
	}
}

fn unix_ms_now() -> i64
{
	use std::time::{ SystemTime, UNIX_EPOCH };
	SystemTime::now()
		.duration_since( UNIX_EPOCH )
		.map( |d| d.as_millis() as i64 )
		.unwrap_or( 0 )
}

/// Fill in each entry's contact from the contacts provider, leaving any that
/// already carry one untouched. The call log stores only numbers; names,
/// labels and avatars come live from contacts so they track edits.
fn resolve_contacts( contacts: &dyn ContactsProvider, entries: &mut [ HistoryEntry ] )
{
	for e in entries.iter_mut()
	{
		if e.contact.is_none()
		{
			e.contact = contacts.lookup_by_number( &e.number ).ok().flatten();
		}
	}
}
