rvoip_client_core/client/
events.rs

1//! Event handling for the client-core library
2//! 
3//! This module contains the event handler that bridges session-core events
4//! to client-core events, providing a clean abstraction for applications.
5
6use std::sync::Arc;
7use std::collections::HashMap;
8use tokio::sync::RwLock;
9use dashmap::DashMap;
10use chrono::Utc;
11
12// Import session-core types
13use rvoip_session_core::{
14    api::{
15        types::{SessionId, CallSession, CallState, IncomingCall, CallDecision},
16        handlers::CallHandler,
17    },
18};
19
20// Import client-core types
21use crate::{
22    call::{CallId, CallInfo, CallDirection},
23    events::{ClientEventHandler, IncomingCallInfo, CallStatusInfo},
24};
25
26// All types are re-exported from the main events module
27
28/// Internal call handler that bridges session-core events to client-core events
29/// 
30/// This handler receives events from the session-core layer and translates them
31/// into client-core events that applications can consume. It manages mappings
32/// between session IDs and call IDs, tracks call state, and forwards events
33/// to registered event handlers.
34/// 
35/// # Architecture
36/// 
37/// The handler maintains several mappings:
38/// - Session ID ↔ Call ID mapping for event translation
39/// - Call information storage with extended metadata
40/// - Incoming call storage for deferred acceptance/rejection
41/// - Event broadcasting through multiple channels
42/// 
43/// # Examples
44/// 
45/// ```rust
46/// use rvoip_client_core::client::events::ClientCallHandler;
47/// use rvoip_session_core::CallHandler;
48/// use std::sync::Arc;
49/// use dashmap::DashMap;
50/// 
51/// let handler = ClientCallHandler::new(
52///     Arc::new(DashMap::new()), // call_mapping
53///     Arc::new(DashMap::new()), // session_mapping  
54///     Arc::new(DashMap::new()), // call_info
55///     Arc::new(DashMap::new()), // incoming_calls
56/// );
57/// ```
58pub struct ClientCallHandler {
59    /// Client event handler for forwarding processed events to applications
60    /// 
61    /// This optional handler receives high-level client events after they have been
62    /// processed and enriched by this bridge. Applications can register handlers
63    /// to receive notifications about incoming calls, state changes, etc.
64    pub client_event_handler: Arc<RwLock<Option<Arc<dyn ClientEventHandler>>>>,
65    
66    /// Mapping from session-core SessionId to client-core CallId
67    /// 
68    /// This bidirectional mapping allows the handler to translate between
69    /// session-core's internal session identifiers and the client-facing call IDs
70    /// that applications use to reference calls.
71    pub call_mapping: Arc<DashMap<SessionId, CallId>>,
72    
73    /// Reverse mapping from client-core CallId to session-core SessionId
74    /// 
75    /// Provides efficient lookup in the opposite direction from call_mapping,
76    /// allowing quick translation from client call IDs to session IDs when
77    /// making session-core API calls.
78    pub session_mapping: Arc<DashMap<CallId, SessionId>>,
79    
80    /// Enhanced call information storage with extended metadata
81    /// 
82    /// Stores comprehensive call information including state, timing data,
83    /// participant details, and custom metadata. This information persists
84    /// throughout the call lifecycle and can be used for history and reporting.
85    pub call_info: Arc<DashMap<CallId, CallInfo>>,
86    
87    /// Storage for incoming calls awaiting acceptance or rejection
88    /// 
89    /// When incoming calls arrive, they are stored here until the application
90    /// decides whether to accept or reject them. This allows for deferred
91    /// call handling and provides access to full call details for decision making.
92    pub incoming_calls: Arc<DashMap<CallId, IncomingCall>>,
93    
94    /// Optional broadcast channel for real-time event streaming
95    /// 
96    /// If configured, events are broadcast through this channel in addition to
97    /// being sent to the registered event handler. This allows multiple consumers
98    /// to receive events independently.
99    pub event_tx: Option<tokio::sync::broadcast::Sender<crate::events::ClientEvent>>,
100    
101    /// Channel for notifying when calls become established
102    /// 
103    /// This channel is used to notify ClientManager when a call transitions to 
104    /// the Connected state, allowing it to set up audio frame subscription.
105    pub(crate) call_established_tx: Option<tokio::sync::mpsc::UnboundedSender<CallId>>,
106}
107
108impl std::fmt::Debug for ClientCallHandler {
109    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110        f.debug_struct("ClientCallHandler")
111            .field("client_event_handler", &"<event handler>")
112            .field("call_mapping", &self.call_mapping)
113            .field("session_mapping", &self.session_mapping)
114            .field("call_info", &self.call_info)
115            .field("incoming_calls", &self.incoming_calls)
116            .finish()
117    }
118}
119
120impl ClientCallHandler {
121    /// Create a new ClientCallHandler with required mappings and storage
122    /// 
123    /// This constructor initializes the handler with the necessary data structures
124    /// for managing call state and event translation between session-core and client-core.
125    /// 
126    /// # Arguments
127    /// 
128    /// * `call_mapping` - Bidirectional mapping between session IDs and call IDs
129    /// * `session_mapping` - Reverse mapping for efficient lookups  
130    /// * `call_info` - Storage for comprehensive call information and metadata
131    /// * `incoming_calls` - Storage for pending incoming calls
132    /// 
133    /// # Examples
134    /// 
135    /// ```rust
136    /// use rvoip_client_core::client::events::ClientCallHandler;
137    /// use std::sync::Arc;
138    /// use dashmap::DashMap;
139    /// 
140    /// let handler = ClientCallHandler::new(
141    ///     Arc::new(DashMap::new()),
142    ///     Arc::new(DashMap::new()),
143    ///     Arc::new(DashMap::new()),
144    ///     Arc::new(DashMap::new()),
145    /// );
146    /// ```
147    pub fn new(
148        call_mapping: Arc<DashMap<SessionId, CallId>>,
149        session_mapping: Arc<DashMap<CallId, SessionId>>,
150        call_info: Arc<DashMap<CallId, CallInfo>>,
151        incoming_calls: Arc<DashMap<CallId, IncomingCall>>,
152    ) -> Self {
153        Self {
154            client_event_handler: Arc::new(RwLock::new(None)),
155            call_mapping,
156            session_mapping,
157            call_info,
158            incoming_calls,
159            event_tx: None,
160            call_established_tx: None,
161        }
162    }
163    
164    /// Configure the handler with an event broadcast channel
165    /// 
166    /// This method adds broadcast capability to the handler, allowing events
167    /// to be sent to multiple consumers through a tokio broadcast channel.
168    /// Events will be sent to both the registered event handler and the broadcast channel.
169    /// 
170    /// # Arguments
171    /// 
172    /// * `event_tx` - Broadcast sender for streaming events to multiple consumers
173    /// 
174    /// # Examples
175    /// 
176    /// ```rust
177    /// use rvoip_client_core::client::events::ClientCallHandler;
178    /// use std::sync::Arc;
179    /// use dashmap::DashMap;
180    /// 
181    /// let (tx, _rx) = tokio::sync::broadcast::channel(100);
182    /// let handler = ClientCallHandler::new(
183    ///     Arc::new(DashMap::new()),
184    ///     Arc::new(DashMap::new()),
185    ///     Arc::new(DashMap::new()),
186    ///     Arc::new(DashMap::new()),
187    /// ).with_event_tx(tx);
188    /// ```
189    pub fn with_event_tx(mut self, event_tx: tokio::sync::broadcast::Sender<crate::events::ClientEvent>) -> Self {
190        self.event_tx = Some(event_tx);
191        self
192    }
193    
194    /// Configure the handler with a call established notification channel
195    /// 
196    /// This internal method is used by ClientManager to provide a channel
197    /// for receiving notifications when calls transition to the Connected state.
198    pub(crate) fn with_call_established_tx(mut self, tx: tokio::sync::mpsc::UnboundedSender<CallId>) -> Self {
199        self.call_established_tx = Some(tx);
200        self
201    }
202    
203    /// Register an event handler to receive processed client events
204    /// 
205    /// This method sets the application-level event handler that will receive
206    /// high-level client events after they have been processed and enriched by this bridge.
207    /// The handler will be called for incoming calls, state changes, media events, etc.
208    /// 
209    /// # Arguments
210    /// 
211    /// * `handler` - The event handler implementation to register
212    /// 
213    /// # Examples
214    /// 
215    /// ```rust
216    /// # use rvoip_client_core::client::events::ClientCallHandler;
217    /// # use rvoip_client_core::events::ClientEventHandler;
218    /// # use std::sync::Arc;
219    /// # use dashmap::DashMap;
220    /// # struct MyEventHandler;
221    /// # #[async_trait::async_trait]
222    /// # impl ClientEventHandler for MyEventHandler {
223    /// #     async fn on_incoming_call(&self, _info: rvoip_client_core::events::IncomingCallInfo) -> rvoip_client_core::events::CallAction {
224    /// #         rvoip_client_core::events::CallAction::Accept
225    /// #     }
226    /// #     async fn on_call_state_changed(&self, _info: rvoip_client_core::events::CallStatusInfo) {}
227    /// #     async fn on_media_event(&self, _info: rvoip_client_core::events::MediaEventInfo) {}
228    /// #     async fn on_registration_status_changed(&self, _info: rvoip_client_core::events::RegistrationStatusInfo) {}
229    /// # }
230    /// # #[tokio::main]
231    /// # async fn main() {
232    /// let handler = ClientCallHandler::new(
233    ///     Arc::new(DashMap::new()),
234    ///     Arc::new(DashMap::new()),
235    ///     Arc::new(DashMap::new()),
236    ///     Arc::new(DashMap::new()),
237    /// );
238    /// 
239    /// let event_handler = Arc::new(MyEventHandler);
240    /// handler.set_event_handler(event_handler).await;
241    /// # }
242    /// ```
243    pub async fn set_event_handler(&self, handler: Arc<dyn ClientEventHandler>) {
244        *self.client_event_handler.write().await = Some(handler);
245    }
246    
247    /// Store an IncomingCall object for later use
248    /// 
249    /// This method stores an incoming call in the handler's storage, allowing it to be
250    /// retrieved later when the application decides to accept or reject the call.
251    /// This enables deferred call handling where the application can examine call
252    /// details before making a decision.
253    /// 
254    /// # Arguments
255    /// 
256    /// * `call_id` - The client-core call ID for this incoming call
257    /// * `incoming_call` - The session-core IncomingCall object with full details
258    /// 
259    /// # Examples
260    /// 
261    /// ```rust
262    /// # use rvoip_client_core::client::events::ClientCallHandler;
263    /// # use rvoip_client_core::call::CallId;
264    /// # use rvoip_session_core::api::types::IncomingCall;
265    /// # use std::sync::Arc;
266    /// # use dashmap::DashMap;
267    /// # #[tokio::main]
268    /// # async fn main() {
269    /// let handler = ClientCallHandler::new(
270    ///     Arc::new(DashMap::new()),
271    ///     Arc::new(DashMap::new()),
272    ///     Arc::new(DashMap::new()),
273    ///     Arc::new(DashMap::new()),
274    /// );
275    /// 
276    /// let call_id = CallId::new_v4();
277    /// # let incoming_call = IncomingCall {
278    /// #     id: rvoip_session_core::api::types::SessionId("test".to_string()),
279    /// #     from: "sip:caller@example.com".to_string(),
280    /// #     to: "sip:callee@example.com".to_string(),
281    /// #     sdp: None,
282    /// #     headers: std::collections::HashMap::new(),
283    /// #     received_at: std::time::Instant::now(),
284    /// # };
285    /// handler.store_incoming_call(call_id, incoming_call).await;
286    /// # }
287    /// ```
288    pub async fn store_incoming_call(&self, call_id: CallId, incoming_call: IncomingCall) {
289        self.incoming_calls.insert(call_id, incoming_call);
290    }
291    
292    /// Retrieve a stored IncomingCall object
293    /// 
294    /// This method retrieves a previously stored incoming call by its call ID.
295    /// This is useful when the application needs to access the full incoming call
296    /// details (SDP, headers, etc.) when making accept/reject decisions or during
297    /// call processing.
298    /// 
299    /// # Arguments
300    /// 
301    /// * `call_id` - The client-core call ID to retrieve the incoming call for
302    /// 
303    /// # Returns
304    /// 
305    /// `Some(IncomingCall)` if a stored incoming call exists for the given call ID,
306    /// `None` if no incoming call is found or if the call has already been processed.
307    /// 
308    /// # Examples
309    /// 
310    /// ```rust
311    /// # use rvoip_client_core::client::events::ClientCallHandler;
312    /// # use rvoip_client_core::call::CallId;
313    /// # use std::sync::Arc;
314    /// # use dashmap::DashMap;
315    /// # #[tokio::main]
316    /// # async fn main() {
317    /// let handler = ClientCallHandler::new(
318    ///     Arc::new(DashMap::new()),
319    ///     Arc::new(DashMap::new()),
320    ///     Arc::new(DashMap::new()),
321    ///     Arc::new(DashMap::new()),
322    /// );
323    /// 
324    /// let call_id = CallId::new_v4();
325    /// 
326    /// // Retrieve incoming call details
327    /// if let Some(incoming_call) = handler.get_incoming_call(&call_id).await {
328    ///     println!("Found incoming call from: {}", incoming_call.from);
329    ///     println!("SDP offer present: {}", incoming_call.sdp.is_some());
330    /// } else {
331    ///     println!("No incoming call found for ID: {}", call_id);
332    /// }
333    /// # }
334    /// ```
335    pub async fn get_incoming_call(&self, call_id: &CallId) -> Option<IncomingCall> {
336        self.incoming_calls.get(call_id).map(|entry| entry.value().clone())
337    }
338    
339    /// Extract display name from SIP URI or headers
340    /// 
341    /// This method attempts to extract a human-readable display name from a SIP URI
342    /// or associated headers. It implements multiple extraction strategies to handle
343    /// various SIP message formats and header configurations.
344    /// 
345    /// # Arguments
346    /// 
347    /// * `uri` - The SIP URI to extract display name from (e.g., "Alice Smith" <sip:alice@example.com>)
348    /// * `headers` - SIP message headers that may contain display name information
349    /// 
350    /// # Returns
351    /// 
352    /// `Some(String)` containing the extracted display name if found, `None` if no
353    /// display name could be extracted from the URI or headers.
354    /// 
355    /// # Extraction Strategy
356    /// 
357    /// 1. Check for quoted display name in URI: `"Display Name" <sip:user@domain>`
358    /// 2. Check for unquoted display name before angle brackets: `Display Name <sip:user@domain>`
359    /// 3. Check From header for display name using the same strategies
360    /// 
361    /// # Examples
362    /// 
363    /// ```rust
364    /// # use rvoip_client_core::client::events::ClientCallHandler;
365    /// # use std::sync::Arc;
366    /// # use std::collections::HashMap;
367    /// # use dashmap::DashMap;
368    /// let handler = ClientCallHandler::new(
369    ///     Arc::new(DashMap::new()),
370    ///     Arc::new(DashMap::new()),
371    ///     Arc::new(DashMap::new()),
372    ///     Arc::new(DashMap::new()),
373    /// );
374    /// 
375    /// let mut headers = HashMap::new();
376    /// headers.insert("From".to_string(), "\"Alice Smith\" <sip:alice@example.com>".to_string());
377    /// 
378    /// // Extract from quoted URI
379    /// let display_name = handler.extract_display_name(
380    ///     "\"Alice Smith\" <sip:alice@example.com>", 
381    ///     &headers
382    /// );
383    /// assert_eq!(display_name, Some("Alice Smith".to_string()));
384    /// 
385    /// // Extract from unquoted URI
386    /// let display_name = handler.extract_display_name(
387    ///     "Bob Jones <sip:bob@example.com>", 
388    ///     &HashMap::new()
389    /// );
390    /// assert_eq!(display_name, Some("Bob Jones".to_string()));
391    /// 
392    /// // No display name available
393    /// let display_name = handler.extract_display_name(
394    ///     "sip:carol@example.com", 
395    ///     &HashMap::new()
396    /// );
397    /// assert_eq!(display_name, None);
398    /// ```
399    pub fn extract_display_name(&self, uri: &str, headers: &HashMap<String, String>) -> Option<String> {
400        // First try to extract from URI (e.g., "Display Name" <sip:user@domain>)
401        if let Some(start) = uri.find('"') {
402            if let Some(end) = uri[start + 1..].find('"') {
403                let display_name = &uri[start + 1..start + 1 + end];
404                if !display_name.is_empty() {
405                    return Some(display_name.to_string());
406                }
407            }
408        }
409        
410        // Try display name before < in URI
411        if let Some(angle_pos) = uri.find('<') {
412            let potential_name = uri[..angle_pos].trim();
413            if !potential_name.is_empty() && !potential_name.starts_with("sip:") {
414                return Some(potential_name.to_string());
415            }
416        }
417        
418        // Try From header display name
419        if let Some(from_header) = headers.get("From") {
420            return self.extract_display_name_from_header(from_header);
421        }
422        
423        None
424    }
425    
426    /// Extract display name from a SIP header string
427    /// 
428    /// This method extracts a human-readable display name from a SIP header value,
429    /// typically the From or To header. It handles both quoted and unquoted display
430    /// name formats commonly found in SIP messages.
431    /// 
432    /// # Arguments
433    /// 
434    /// * `header` - The SIP header value to parse for display name information
435    /// 
436    /// # Returns
437    /// 
438    /// `Some(String)` containing the extracted display name if found, `None` if no
439    /// display name could be extracted from the header.
440    /// 
441    /// # Supported Formats
442    /// 
443    /// - Quoted display name: `"Alice Smith" <sip:alice@example.com>`
444    /// - Unquoted display name: `Alice Smith <sip:alice@example.com>`
445    /// - Plain SIP URI: `sip:alice@example.com` (returns None)
446    /// 
447    /// # Examples
448    /// 
449    /// ```rust
450    /// # use rvoip_client_core::client::events::ClientCallHandler;
451    /// # use std::sync::Arc;
452    /// # use dashmap::DashMap;
453    /// let handler = ClientCallHandler::new(
454    ///     Arc::new(DashMap::new()),
455    ///     Arc::new(DashMap::new()),
456    ///     Arc::new(DashMap::new()),
457    ///     Arc::new(DashMap::new()),
458    /// );
459    /// 
460    /// // Extract from quoted header
461    /// let name = handler.extract_display_name_from_header(
462    ///     "\"Alice Smith\" <sip:alice@example.com>"
463    /// );
464    /// assert_eq!(name, Some("Alice Smith".to_string()));
465    /// 
466    /// // Extract from unquoted header
467    /// let name = handler.extract_display_name_from_header(
468    ///     "Bob Jones <sip:bob@example.com>"
469    /// );
470    /// assert_eq!(name, Some("Bob Jones".to_string()));
471    /// 
472    /// // No display name in plain URI
473    /// let name = handler.extract_display_name_from_header(
474    ///     "sip:carol@example.com"
475    /// );
476    /// assert_eq!(name, None);
477    /// ```
478    pub fn extract_display_name_from_header(&self, header: &str) -> Option<String> {
479        if let Some(start) = header.find('"') {
480            if let Some(end) = header[start + 1..].find('"') {
481                let display_name = &header[start + 1..start + 1 + end];
482                if !display_name.is_empty() {
483                    return Some(display_name.to_string());
484                }
485            }
486        }
487        
488        if let Some(angle_pos) = header.find('<') {
489            let potential_name = header[..angle_pos].trim();
490            if !potential_name.is_empty() && !potential_name.starts_with("sip:") {
491                return Some(potential_name.to_string());
492            }
493        }
494        
495        None
496    }
497    
498    /// Extract call subject from SIP message headers
499    /// 
500    /// This method extracts the subject/purpose of a call from SIP message headers.
501    /// The subject provides contextual information about the call and is typically
502    /// used for displaying call purpose in user interfaces or for call routing decisions.
503    /// 
504    /// # Arguments
505    /// 
506    /// * `headers` - HashMap containing SIP message headers to search for subject information
507    /// 
508    /// # Returns
509    /// 
510    /// `Some(String)` containing the subject text if found and non-empty,
511    /// `None` if no subject header exists or if the subject is empty.
512    /// 
513    /// # Header Priority
514    /// 
515    /// The method searches for subject information in the following order:
516    /// 1. "Subject" header (standard case)
517    /// 2. "subject" header (lowercase variant)
518    /// 
519    /// # Examples
520    /// 
521    /// ```rust
522    /// # use rvoip_client_core::client::events::ClientCallHandler;
523    /// # use std::sync::Arc;
524    /// # use std::collections::HashMap;
525    /// # use dashmap::DashMap;
526    /// let handler = ClientCallHandler::new(
527    ///     Arc::new(DashMap::new()),
528    ///     Arc::new(DashMap::new()),
529    ///     Arc::new(DashMap::new()),
530    ///     Arc::new(DashMap::new()),
531    /// );
532    /// 
533    /// let mut headers = HashMap::new();
534    /// headers.insert("Subject".to_string(), "Conference Call".to_string());
535    /// 
536    /// // Extract subject from headers
537    /// let subject = handler.extract_subject(&headers);
538    /// assert_eq!(subject, Some("Conference Call".to_string()));
539    /// 
540    /// // Empty subject returns None
541    /// let mut empty_headers = HashMap::new();
542    /// empty_headers.insert("Subject".to_string(), "".to_string());
543    /// let subject = handler.extract_subject(&empty_headers);
544    /// assert_eq!(subject, None);
545    /// 
546    /// // No subject header returns None
547    /// let subject = handler.extract_subject(&HashMap::new());
548    /// assert_eq!(subject, None);
549    /// ```
550    pub fn extract_subject(&self, headers: &HashMap<String, String>) -> Option<String> {
551        headers.get("Subject")
552            .or_else(|| headers.get("subject"))
553            .cloned()
554            .filter(|s| !s.is_empty())
555    }
556    
557    /// Extract SIP Call-ID from message headers
558    /// 
559    /// This method extracts the unique Call-ID identifier from SIP message headers.
560    /// The Call-ID is a mandatory header in SIP messages that uniquely identifies
561    /// a call dialog and remains constant throughout the entire call session.
562    /// 
563    /// # Arguments
564    /// 
565    /// * `headers` - HashMap containing SIP message headers to search for Call-ID
566    /// 
567    /// # Returns
568    /// 
569    /// `Some(String)` containing the Call-ID value if found,
570    /// `None` if no Call-ID header exists in the message.
571    /// 
572    /// # Header Priority
573    /// 
574    /// The method searches for Call-ID information in the following order:
575    /// 1. "Call-ID" header (standard case)
576    /// 2. "call-id" header (lowercase variant)
577    /// 
578    /// # SIP Specification
579    /// 
580    /// According to RFC 3261, the Call-ID header is mandatory in all SIP requests
581    /// and responses. It consists of a locally unique identifier followed by an
582    /// "@" sign and a globally unique identifier (usually the host domain).
583    /// 
584    /// # Examples
585    /// 
586    /// ```rust
587    /// # use rvoip_client_core::client::events::ClientCallHandler;
588    /// # use std::sync::Arc;
589    /// # use std::collections::HashMap;
590    /// # use dashmap::DashMap;
591    /// let handler = ClientCallHandler::new(
592    ///     Arc::new(DashMap::new()),
593    ///     Arc::new(DashMap::new()),
594    ///     Arc::new(DashMap::new()),
595    ///     Arc::new(DashMap::new()),
596    /// );
597    /// 
598    /// let mut headers = HashMap::new();
599    /// headers.insert("Call-ID".to_string(), "1234567890@example.com".to_string());
600    /// 
601    /// // Extract Call-ID from headers
602    /// let call_id = handler.extract_call_id(&headers);
603    /// assert_eq!(call_id, Some("1234567890@example.com".to_string()));
604    /// 
605    /// // Case-insensitive header lookup
606    /// let mut headers_lower = HashMap::new();
607    /// headers_lower.insert("call-id".to_string(), "abcdef@sip.example.org".to_string());
608    /// let call_id = handler.extract_call_id(&headers_lower);
609    /// assert_eq!(call_id, Some("abcdef@sip.example.org".to_string()));
610    /// 
611    /// // No Call-ID header returns None
612    /// let call_id = handler.extract_call_id(&HashMap::new());
613    /// assert_eq!(call_id, None);
614    /// ```
615    pub fn extract_call_id(&self, headers: &HashMap<String, String>) -> Option<String> {
616        headers.get("Call-ID")
617            .or_else(|| headers.get("call-id"))
618            .cloned()
619    }
620    
621    /// Update stored call information with enhanced session data
622    /// 
623    /// This method synchronizes call information stored in the client-core layer
624    /// with the current state from the session-core layer. It handles state transitions,
625    /// timestamp updates, and event emission when significant changes occur.
626    /// 
627    /// # Arguments
628    /// 
629    /// * `call_id` - The client-core call ID to update information for
630    /// * `session` - The session-core CallSession object containing current state and metadata
631    /// 
632    /// # Behavior
633    /// 
634    /// The method performs the following operations:
635    /// 1. Maps session-core state to client-core state representation
636    /// 2. Updates timestamps for significant state transitions (connected, ended)
637    /// 3. Emits state change events to registered handlers when state changes
638    /// 4. Preserves historical information and call metadata
639    /// 
640    /// # State Transitions
641    /// 
642    /// Special handling is applied for specific state transitions:
643    /// - **Connected**: Sets `connected_at` timestamp if not already set
644    /// - **Terminated/Failed/Cancelled**: Sets `ended_at` timestamp if not already set
645    /// - **Other states**: Updates state without timestamp modifications
646    /// 
647    /// # Event Emission
648    /// 
649    /// When a state change is detected, the method automatically emits a `CallStatusInfo`
650    /// event to the registered client event handler, providing:
651    /// - Current and previous states
652    /// - Transition timestamp
653    /// - Call identification information
654    /// 
655    /// # Examples
656    /// 
657    /// ```rust
658    /// # use rvoip_client_core::client::events::ClientCallHandler;
659    /// # use rvoip_client_core::call::CallId;
660    /// # use rvoip_session_core::api::types::{CallSession, CallState, SessionId};
661    /// # use std::sync::Arc;
662    /// # use dashmap::DashMap;
663    /// # #[tokio::main]
664    /// # async fn main() {
665    /// let handler = ClientCallHandler::new(
666    ///     Arc::new(DashMap::new()),
667    ///     Arc::new(DashMap::new()),
668    ///     Arc::new(DashMap::new()),
669    ///     Arc::new(DashMap::new()),
670    /// );
671    /// 
672    /// let call_id = CallId::new_v4();
673    /// # let session = CallSession {
674    /// #     id: SessionId("session123".to_string()),
675    /// #     from: "sip:alice@example.com".to_string(),
676    /// #     to: "sip:bob@example.com".to_string(),
677    /// #     state: CallState::Active,
678    /// #     started_at: Some(std::time::Instant::now()),
679    /// # };
680    /// 
681    /// // Update call info when session state changes
682    /// handler.update_call_info_from_session(call_id, &session).await;
683    /// 
684    /// // The method will:
685    /// // 1. Map CallState::Active to client-core Connected state
686    /// // 2. Set connected_at timestamp if transitioning to Connected
687    /// // 3. Emit state change event to registered handlers
688    /// # }
689    /// ```
690    /// 
691    /// # Thread Safety
692    /// 
693    /// This method is async and thread-safe. It uses atomic operations on the
694    /// underlying DashMap storage and properly handles concurrent access to call information.
695    pub async fn update_call_info_from_session(&self, call_id: CallId, session: &CallSession) {
696        if let Some(mut call_info_ref) = self.call_info.get_mut(&call_id) {
697            // Update state if it changed
698            let new_client_state = self.map_session_state_to_client_state(&session.state);
699            let old_state = call_info_ref.state.clone();
700            
701            if new_client_state != old_state {
702                // Update timestamps based on state transition
703                match new_client_state {
704                    crate::call::CallState::Connected => {
705                        if call_info_ref.connected_at.is_none() {
706                            call_info_ref.connected_at = Some(Utc::now());
707                        }
708                    }
709                    crate::call::CallState::Terminated | 
710                    crate::call::CallState::Failed | 
711                    crate::call::CallState::Cancelled => {
712                        if call_info_ref.ended_at.is_none() {
713                            call_info_ref.ended_at = Some(Utc::now());
714                        }
715                    }
716                    _ => {}
717                }
718                
719                call_info_ref.state = new_client_state.clone();
720                
721                // Emit state change event
722                if let Some(handler) = self.client_event_handler.read().await.as_ref() {
723                    let status_info = CallStatusInfo {
724                        call_id,
725                        new_state: new_client_state,
726                        previous_state: Some(old_state),
727                        reason: None,
728                        timestamp: Utc::now(),
729                    };
730                    handler.on_call_state_changed(status_info).await;
731                }
732            }
733        }
734    }
735    
736    /// Map session-core CallState to client-core CallState with enhanced logic
737    /// 
738    /// This method translates between the internal session-core call state representation
739    /// and the client-facing call state representation. It provides a clean abstraction
740    /// layer and applies enhanced logic for complex state mappings.
741    /// 
742    /// # Arguments
743    /// 
744    /// * `session_state` - The session-core CallState to map to client-core representation
745    /// 
746    /// # Returns
747    /// 
748    /// The corresponding client-core CallState that represents the same logical state
749    /// but with client-appropriate semantics and naming.
750    /// 
751    /// # State Mapping Logic
752    /// 
753    /// The mapping applies the following transformations:
754    /// 
755    /// | Session-Core State | Client-Core State | Notes |
756    /// |-------------------|------------------|--------|
757    /// | `Initiating` | `Initiating` | Direct mapping |
758    /// | `Ringing` | `Ringing` | Direct mapping |
759    /// | `Active` | `Connected` | Semantic clarity for client |
760    /// | `OnHold` | `Connected` | Still connected, just on hold |
761    /// | `Transferring` | `Proceeding` | Transfer in progress |
762    /// | `Terminating` | `Terminating` | Direct mapping |
763    /// | `Terminated` | `Terminated` | Direct mapping |
764    /// | `Cancelled` | `Cancelled` | Direct mapping |
765    /// | `Failed(reason)` | `Failed` | Logs reason, maps to simple Failed |
766    /// 
767    /// # Enhanced Logic
768    /// 
769    /// - **OnHold Handling**: Calls on hold are still considered "Connected" from the client
770    ///   perspective, as the media session is established and can be resumed.
771    /// - **Transfer Handling**: Calls being transferred are mapped to "Proceeding" to indicate
772    ///   ongoing call setup activities.
773    /// - **Failure Handling**: Failed states with reasons are logged for debugging but
774    ///   simplified to a single Failed state for client consumption.
775    /// 
776    /// # Examples
777    /// 
778    /// ```rust
779    /// # use rvoip_client_core::client::events::ClientCallHandler;
780    /// # use rvoip_session_core::api::types::CallState;
781    /// # use std::sync::Arc;
782    /// # use dashmap::DashMap;
783    /// let handler = ClientCallHandler::new(
784    ///     Arc::new(DashMap::new()),
785    ///     Arc::new(DashMap::new()),
786    ///     Arc::new(DashMap::new()),
787    ///     Arc::new(DashMap::new()),
788    /// );
789    /// 
790    /// // Map active session to connected client state
791    /// let session_state = CallState::Active;
792    /// let client_state = handler.map_session_state_to_client_state(&session_state);
793    /// assert_eq!(client_state, rvoip_client_core::call::CallState::Connected);
794    /// 
795    /// // Map on-hold session to still connected client state
796    /// let session_state = CallState::OnHold;
797    /// let client_state = handler.map_session_state_to_client_state(&session_state);
798    /// assert_eq!(client_state, rvoip_client_core::call::CallState::Connected);
799    /// 
800    /// // Map failed session to failed client state (reason is logged)
801    /// let session_state = CallState::Failed("Network timeout".to_string());
802    /// let client_state = handler.map_session_state_to_client_state(&session_state);
803    /// assert_eq!(client_state, rvoip_client_core::call::CallState::Failed);
804    /// ```
805    /// 
806    /// # Logging
807    /// 
808    /// The method logs debug information for failed states to assist with troubleshooting
809    /// while providing a clean interface to client applications.
810    pub fn map_session_state_to_client_state(&self, session_state: &CallState) -> crate::call::CallState {
811        match session_state {
812            CallState::Initiating => crate::call::CallState::Initiating,
813            CallState::Ringing => crate::call::CallState::Ringing,
814            CallState::Active => crate::call::CallState::Connected,
815            CallState::OnHold => crate::call::CallState::Connected, // Still connected, just on hold
816            CallState::Transferring => crate::call::CallState::Proceeding,
817            CallState::Terminating => crate::call::CallState::Terminating,
818            CallState::Terminated => crate::call::CallState::Terminated,
819            CallState::Cancelled => crate::call::CallState::Cancelled,
820            CallState::Failed(reason) => {
821                tracing::debug!("Call failed with reason: {}", reason);
822                crate::call::CallState::Failed
823            }
824        }
825    }
826}
827
828/// Implementation of session-core CallHandler trait for ClientCallHandler
829/// 
830/// This trait implementation bridges session-core events to client-core events,
831/// providing the core event translation and handling logic. The implementation
832/// receives low-level session events and transforms them into high-level client
833/// events that applications can easily consume.
834/// 
835/// # Event Flow
836/// 
837/// 1. **Session-core events** arrive through this trait implementation
838/// 2. **Event translation** maps session concepts to client concepts
839/// 3. **State management** updates call information and mappings
840/// 4. **Client events** are emitted to registered handlers and broadcast channels
841/// 5. **Cleanup** removes temporary state when calls complete
842/// 
843/// # Thread Safety
844/// 
845/// All methods in this implementation are async and thread-safe, using
846/// atomic operations and concurrent data structures for state management.
847#[async_trait::async_trait]
848impl CallHandler for ClientCallHandler {
849    /// Handle incoming call from session-core layer
850    /// 
851    /// This method is called by session-core when a new incoming call arrives.
852    /// It performs comprehensive call processing including ID mapping, metadata extraction,
853    /// event emission, and decision routing to the application layer.
854    /// 
855    /// # Arguments
856    /// 
857    /// * `call` - The IncomingCall object from session-core containing all call details
858    /// 
859    /// # Returns
860    /// 
861    /// `CallDecision` indicating how session-core should handle the call:
862    /// - `Accept(sdp)` - Accept the call with optional SDP answer
863    /// - `Reject(reason)` - Reject the call with a reason string  
864    /// - `Defer` - Defer the decision (used for ignore action)
865    /// 
866    /// # Processing Flow
867    /// 
868    /// 1. **ID Mapping**: Creates new client call ID and establishes bidirectional mapping
869    /// 2. **Metadata Extraction**: Extracts display names, subject, and SIP headers
870    /// 3. **Call Info Creation**: Creates comprehensive CallInfo with all available data
871    /// 4. **Event Broadcasting**: Emits incoming call event to broadcast channel if configured
872    /// 5. **Handler Consultation**: Forwards to application event handler for decision
873    /// 6. **Decision Translation**: Maps client decision back to session-core format
874    /// 
875    /// # SDP Handling
876    /// 
877    /// - If the incoming call contains an SDP offer and the application accepts,
878    ///   the method allows session-core to generate the SDP answer automatically
879    /// - If no SDP offer is present, the call is accepted without SDP negotiation
880    /// - Complex SDP scenarios are handled transparently by session-core
881    /// 
882    /// # State Management
883    /// 
884    /// - Call starts in `IncomingPending` state
885    /// - Full call information is stored for history and reporting
886    /// - Incoming call object is stored for later access during accept/reject operations
887    /// 
888    /// # Examples
889    /// 
890    /// ```rust
891    /// # use rvoip_client_core::client::events::ClientCallHandler;
892    /// # use rvoip_session_core::api::types::{IncomingCall, CallDecision, SessionId};
893    /// # use rvoip_session_core::CallHandler;
894    /// # use std::sync::Arc;
895    /// # use std::collections::HashMap;
896    /// # use dashmap::DashMap;
897    /// # #[tokio::main]
898    /// # async fn main() {
899    /// let handler = ClientCallHandler::new(
900    ///     Arc::new(DashMap::new()),
901    ///     Arc::new(DashMap::new()),
902    ///     Arc::new(DashMap::new()),
903    ///     Arc::new(DashMap::new()),
904    /// );
905    /// 
906    /// # let incoming_call = IncomingCall {
907    /// #     id: SessionId("session123".to_string()),
908    /// #     from: "\"Alice Smith\" <sip:alice@example.com>".to_string(),
909    /// #     to: "sip:bob@example.com".to_string(),
910    /// #     sdp: Some("v=0...".to_string()),
911    /// #     headers: HashMap::new(),
912    /// #     received_at: std::time::Instant::now(),
913    /// # };
914    /// 
915    /// // This method is called automatically by session-core
916    /// let decision = handler.on_incoming_call(incoming_call).await;
917    /// 
918    /// // The method will:
919    /// // 1. Extract caller display name "Alice Smith"
920    /// // 2. Create call info with all metadata
921    /// // 3. Emit incoming call event to application
922    /// // 4. Return application's accept/reject decision
923    /// # }
924    /// ```
925    async fn on_incoming_call(&self, call: IncomingCall) -> CallDecision {
926        // Map session to call
927        let call_id = CallId::new_v4();
928        self.call_mapping.insert(call.id.clone(), call_id);
929        self.session_mapping.insert(call_id, call.id.clone());
930        
931        // Store the IncomingCall for later use in answer/reject
932        self.incoming_calls.insert(call_id, call.clone());
933        
934        // Enhanced call info extraction
935        let caller_display_name = self.extract_display_name(&call.from, &call.headers);
936        let subject = self.extract_subject(&call.headers);
937        let sip_call_id = self.extract_call_id(&call.headers)
938            .unwrap_or_else(|| call.id.0.clone());
939        
940        // Create comprehensive call info
941        let call_info = CallInfo {
942            call_id,
943            state: crate::call::CallState::IncomingPending,
944            direction: CallDirection::Incoming,
945            local_uri: call.to.clone(),
946            remote_uri: call.from.clone(),
947            remote_display_name: caller_display_name.clone(),
948            subject: subject.clone(),
949            created_at: Utc::now(),
950            connected_at: None,
951            ended_at: None,
952            remote_addr: None, // TODO: Extract from session if available
953            media_session_id: None,
954            sip_call_id,
955            metadata: call.headers.clone(),
956        };
957        
958        // Store call info
959        self.call_info.insert(call_id, call_info.clone());
960        
961        // Create incoming call info for event
962        let incoming_call_info = IncomingCallInfo {
963            call_id,
964            caller_uri: call.from.clone(),
965            callee_uri: call.to.clone(),
966            caller_display_name,
967            subject,
968            created_at: Utc::now(),
969        };
970        
971        // Broadcast event
972        if let Some(event_tx) = &self.event_tx {
973            let _ = event_tx.send(crate::events::ClientEvent::IncomingCall { 
974                info: incoming_call_info.clone(),
975                priority: crate::events::EventPriority::High,
976            });
977        }
978        
979        // Forward to client event handler
980        if let Some(handler) = self.client_event_handler.read().await.as_ref() {
981            let action = handler.on_incoming_call(incoming_call_info).await;
982            match action {
983                crate::events::CallAction::Accept => {
984                    // When Accept is returned, we need to generate SDP answer and accept the call
985                    tracing::info!("Handler returned Accept for call {}, generating SDP answer", call_id);
986                    
987                    // Generate SDP answer if the incoming call has an offer
988                    let sdp_answer = if let Some(_offer) = &call.sdp {
989                        // Use session-core's media control to generate answer
990                        // Note: We need access to the coordinator here, which we don't have directly
991                        // So we'll let session-core handle SDP generation by passing None
992                        // and marking that we need SDP generation
993                        tracing::info!("Incoming call has SDP offer, will generate answer in session-core");
994                        None // Let session-core generate the answer
995                    } else {
996                        tracing::info!("No SDP offer in incoming call, accepting without SDP");
997                        None
998                    };
999                    
1000                    // Return Accept with the SDP (or None to let session-core generate it)
1001                    CallDecision::Accept(sdp_answer)
1002                }
1003                crate::events::CallAction::Reject => CallDecision::Reject("Call rejected by user".to_string()),
1004                crate::events::CallAction::Ignore => CallDecision::Defer,
1005            }
1006        } else {
1007            CallDecision::Reject("No event handler configured".to_string())
1008        }
1009    }
1010    
1011    /// Handle call termination from session-core layer
1012    /// 
1013    /// This method is called by session-core when a call ends, regardless of the cause
1014    /// (user hangup, network failure, timeout, etc.). It performs cleanup operations,
1015    /// updates call statistics, and emits final state change events.
1016    /// 
1017    /// # Arguments
1018    /// 
1019    /// * `session` - The CallSession object containing final call state and metadata
1020    /// * `reason` - Human-readable string describing why the call ended
1021    /// 
1022    /// # Processing Flow
1023    /// 
1024    /// 1. **Call Lookup**: Maps session ID to client call ID
1025    /// 2. **Statistics Update**: Handles connected call counter management
1026    /// 3. **State Finalization**: Updates call info with final state and timestamp
1027    /// 4. **Metadata Preservation**: Stores termination reason and final state
1028    /// 5. **Event Emission**: Broadcasts call ended event to handlers
1029    /// 6. **Cleanup**: Removes active mappings while preserving call history
1030    /// 
1031    /// # Critical Bug Fix
1032    /// 
1033    /// This method includes a critical fix for integer overflow in call statistics.
1034    /// Previously, calls ending through session-core (network timeouts, remote hangup)
1035    /// weren't decrementing the connected_calls counter, leading to overflow issues.
1036    /// 
1037    /// The fix:
1038    /// - Tracks whether the call was in Connected state before termination
1039    /// - Adds metadata flag for the manager to decrement counters appropriately
1040    /// - Prevents statistics corruption from external call termination
1041    /// 
1042    /// # State Management
1043    /// 
1044    /// - Final call state is mapped from session-core to client-core representation
1045    /// - `ended_at` timestamp is set to current time
1046    /// - Termination reason is stored in call metadata for history
1047    /// - Call information is preserved for reporting and analytics
1048    /// 
1049    /// # Cleanup Operations
1050    /// 
1051    /// - Removes active session-to-call and call-to-session mappings
1052    /// - Preserves call_info for historical access and reporting
1053    /// - Cleans up any temporary state related to the call
1054    /// 
1055    /// # Examples
1056    /// 
1057    /// ```rust
1058    /// # use rvoip_client_core::client::events::ClientCallHandler;
1059    /// # use rvoip_session_core::api::types::{CallSession, CallState, SessionId};
1060    /// # use rvoip_session_core::CallHandler;
1061    /// # use std::sync::Arc;
1062    /// # use dashmap::DashMap;
1063    /// # #[tokio::main]
1064    /// # async fn main() {
1065    /// let handler = ClientCallHandler::new(
1066    ///     Arc::new(DashMap::new()),
1067    ///     Arc::new(DashMap::new()),
1068    ///     Arc::new(DashMap::new()),
1069    ///     Arc::new(DashMap::new()),
1070    /// );
1071    /// 
1072    /// # let session = CallSession {
1073    /// #     id: SessionId("session123".to_string()),
1074    /// #     from: "sip:alice@example.com".to_string(),
1075    /// #     to: "sip:bob@example.com".to_string(),
1076    /// #     state: CallState::Terminated,
1077    /// #     started_at: Some(std::time::Instant::now()),
1078    /// # };
1079    /// 
1080    /// // This method is called automatically by session-core
1081    /// handler.on_call_ended(session, "User hangup").await;
1082    /// 
1083    /// // The method will:
1084    /// // 1. Update call info to Terminated state
1085    /// // 2. Set ended_at timestamp
1086    /// // 3. Store "User hangup" in metadata
1087    /// // 4. Emit final state change event
1088    /// // 5. Clean up active mappings
1089    /// # }
1090    /// ```
1091    async fn on_call_ended(&self, session: CallSession, reason: &str) {
1092        // Map session to client call and emit event
1093        if let Some(call_id) = self.call_mapping.get(&session.id).map(|entry| *entry.value()) {
1094            // Check if the call was previously connected to determine if we need to decrement counter
1095            let was_connected = if let Some(call_info) = self.call_info.get(&call_id) {
1096                call_info.state == crate::call::CallState::Connected
1097            } else {
1098                false
1099            };
1100            
1101            // Update call info with final state
1102            if let Some(mut call_info_ref) = self.call_info.get_mut(&call_id) {
1103                call_info_ref.state = self.map_session_state_to_client_state(&session.state);
1104                call_info_ref.ended_at = Some(Utc::now());
1105                
1106                // Add termination reason to metadata
1107                call_info_ref.metadata.insert("termination_reason".to_string(), reason.to_string());
1108            }
1109            
1110            // Update stats - decrement connected_calls if the call was connected
1111            // This fixes the critical integer overflow bug where calls ending through
1112            // session-core (network timeouts, remote hangup, etc.) weren't decrementing the counter
1113            if was_connected {
1114                // We need access to the stats, but we don't have it directly here.
1115                // We'll emit a special event that the manager can handle to update stats.
1116                tracing::debug!("Call {} was connected and ended, should decrement connected_calls counter", call_id);
1117                
1118                // Since we can't access stats directly, we'll add metadata to let
1119                // the manager know to decrement the counter when processing this event
1120                if let Some(mut call_info_ref) = self.call_info.get_mut(&call_id) {
1121                    call_info_ref.metadata.insert("was_connected_when_ended".to_string(), "true".to_string());
1122                }
1123            }
1124            
1125            let status_info = CallStatusInfo {
1126                call_id,
1127                new_state: self.map_session_state_to_client_state(&session.state),
1128                previous_state: None, // TODO: Track previous state
1129                reason: Some(reason.to_string()),
1130                timestamp: Utc::now(),
1131            };
1132            
1133            // Broadcast event
1134            if let Some(event_tx) = &self.event_tx {
1135                let _ = event_tx.send(crate::events::ClientEvent::CallStateChanged { 
1136                    info: status_info.clone(),
1137                    priority: crate::events::EventPriority::Normal,
1138                });
1139            }
1140            
1141            // Forward to client event handler
1142            if let Some(handler) = self.client_event_handler.read().await.as_ref() {
1143                handler.on_call_state_changed(status_info).await;
1144            }
1145            
1146            // Clean up mappings but keep call_info for history
1147            self.call_mapping.remove(&session.id);
1148            self.session_mapping.remove(&call_id);
1149        }
1150    }
1151    
1152    /// Handle successful call establishment from session-core layer
1153    /// 
1154    /// This method is called by session-core when a call is successfully established
1155    /// and media can flow between participants. It represents the transition from
1156    /// call setup to active communication phase.
1157    /// 
1158    /// # Arguments
1159    /// 
1160    /// * `session` - The CallSession object containing established call state
1161    /// * `local_sdp` - Optional SDP offer/answer generated locally
1162    /// * `remote_sdp` - Optional SDP offer/answer received from remote party
1163    /// 
1164    /// # Processing Flow
1165    /// 
1166    /// 1. **Call Lookup**: Maps session ID to client call ID
1167    /// 2. **State Update**: Transitions call to Connected state
1168    /// 3. **Timestamp Recording**: Sets connected_at timestamp for analytics
1169    /// 4. **SDP Storage**: Preserves SDP information in call metadata
1170    /// 5. **Event Emission**: Broadcasts call established event with high priority
1171    /// 6. **Logging**: Records successful establishment for debugging
1172    /// 
1173    /// # SDP Information Management
1174    /// 
1175    /// Both local and remote SDP information is stored in call metadata:
1176    /// - `local_sdp`: The SDP offer or answer generated by the local endpoint
1177    /// - `remote_sdp`: The SDP offer or answer received from the remote endpoint
1178    /// 
1179    /// This information is crucial for:
1180    /// - Media session management and troubleshooting
1181    /// - Codec negotiation analysis
1182    /// - Network path and capability verification
1183    /// - Call quality investigation
1184    /// 
1185    /// # State Transitions
1186    /// 
1187    /// - Updates call state to `Connected`
1188    /// - Sets `connected_at` timestamp if not already set
1189    /// - Preserves call establishment timing for billing and analytics
1190    /// 
1191    /// # Event Priority
1192    /// 
1193    /// Call establishment events are emitted with `High` priority because:
1194    /// - They represent successful completion of call setup
1195    /// - Applications often need immediate notification for UI updates
1196    /// - Statistics and billing systems require prompt notification
1197    /// 
1198    /// # Examples
1199    /// 
1200    /// ```rust
1201    /// # use rvoip_client_core::client::events::ClientCallHandler;
1202    /// # use rvoip_session_core::api::types::{CallSession, CallState, SessionId};
1203    /// # use rvoip_session_core::CallHandler;
1204    /// # use std::sync::Arc;
1205    /// # use dashmap::DashMap;
1206    /// # #[tokio::main]
1207    /// # async fn main() {
1208    /// let handler = ClientCallHandler::new(
1209    ///     Arc::new(DashMap::new()),
1210    ///     Arc::new(DashMap::new()),
1211    ///     Arc::new(DashMap::new()),
1212    ///     Arc::new(DashMap::new()),
1213    /// );
1214    /// 
1215    /// # let session = CallSession {
1216    /// #     id: SessionId("session123".to_string()),
1217    /// #     from: "sip:alice@example.com".to_string(),
1218    /// #     to: "sip:bob@example.com".to_string(),
1219    /// #     state: CallState::Active,
1220    /// #     started_at: Some(std::time::Instant::now()),
1221    /// # };
1222    /// 
1223    /// let local_sdp = Some("v=0\r\no=alice 123456 654321 IN IP4 192.168.1.100\r\n...".to_string());
1224    /// let remote_sdp = Some("v=0\r\no=bob 789012 210987 IN IP4 192.168.1.200\r\n...".to_string());
1225    /// 
1226    /// // This method is called automatically by session-core
1227    /// handler.on_call_established(session, local_sdp, remote_sdp).await;
1228    /// 
1229    /// // The method will:
1230    /// // 1. Update call state to Connected
1231    /// // 2. Set connected_at timestamp
1232    /// // 3. Store both local and remote SDP in metadata
1233    /// // 4. Emit high-priority call established event
1234    /// // 5. Log successful establishment
1235    /// # }
1236    /// ```
1237    async fn on_call_established(&self, session: CallSession, local_sdp: Option<String>, remote_sdp: Option<String>) {
1238        // Map session to client call
1239        if let Some(call_id) = self.call_mapping.get(&session.id).map(|entry| *entry.value()) {
1240            // Update call info with establishment
1241            if let Some(mut call_info_ref) = self.call_info.get_mut(&call_id) {
1242                call_info_ref.state = crate::call::CallState::Connected;
1243                if call_info_ref.connected_at.is_none() {
1244                    call_info_ref.connected_at = Some(Utc::now());
1245                }
1246                
1247                // Store SDP information
1248                if let Some(local_sdp) = &local_sdp {
1249                    call_info_ref.metadata.insert("local_sdp".to_string(), local_sdp.clone());
1250                }
1251                if let Some(remote_sdp) = &remote_sdp {
1252                    call_info_ref.metadata.insert("remote_sdp".to_string(), remote_sdp.clone());
1253                    
1254                    // Process the SDP answer to configure RTP endpoints
1255                    // Note: We don't have direct access to ClientManager here, but that's OK
1256                    // because session-core will handle the SDP processing when it receives
1257                    // the CallAnswered event. The remote SDP is already stored in metadata.
1258                }
1259            }
1260            
1261            let status_info = CallStatusInfo {
1262                call_id,
1263                new_state: crate::call::CallState::Connected,
1264                previous_state: Some(crate::call::CallState::Proceeding),
1265                reason: Some("Call established".to_string()),
1266                timestamp: Utc::now(),
1267            };
1268            
1269            // Broadcast event
1270            if let Some(event_tx) = &self.event_tx {
1271                let _ = event_tx.send(crate::events::ClientEvent::CallStateChanged { 
1272                    info: status_info.clone(),
1273                    priority: crate::events::EventPriority::High,
1274                });
1275            }
1276            
1277            // Forward to client event handler
1278            if let Some(handler) = self.client_event_handler.read().await.as_ref() {
1279                handler.on_call_state_changed(status_info).await;
1280            }
1281            
1282            // Notify about call establishment for audio setup
1283            if let Some(tx) = &self.call_established_tx {
1284                let _ = tx.send(call_id);
1285            }
1286            
1287            tracing::info!("Call {} established with SDP exchange", call_id);
1288        }
1289    }
1290}