//! SRS — Session Recording Server (RFC 7866).
//!
//! Receives SIPREC INVITEs from external SRCs, parses the recording
//! metadata XML, sets up RTPEngine recording sessions, and stores
//! recordings + metadata.

pub mod storage;

use std::fmt;
use std::path::PathBuf;
use std::time::Instant;

use dashmap::DashMap;
use serde::Serialize;
use tracing::{info, warn};

use crate::config::SrsConfig;
use crate::siprec::metadata::RecordingMetadata;

// ---------------------------------------------------------------------------
// Session state
// ---------------------------------------------------------------------------

/// State of an SRS recording session.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SrsSessionState {
    /// SIPREC INVITE received, RTPEngine being set up.
    Pending,
    /// RTPEngine recording active, media flowing.
    Recording,
    /// BYE received from SRC, tearing down.
    Stopping,
    /// Recording complete, metadata written.
    Completed,
    /// Recording failed.
    Failed(String),
}

impl fmt::Display for SrsSessionState {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Pending => write!(formatter, "pending"),
            Self::Recording => write!(formatter, "recording"),
            Self::Stopping => write!(formatter, "stopping"),
            Self::Completed => write!(formatter, "completed"),
            Self::Failed(reason) => write!(formatter, "failed: {reason}"),
        }
    }
}

// ---------------------------------------------------------------------------
// SRS session
// ---------------------------------------------------------------------------

/// An active recording session managed by the SRS.
#[derive(Debug)]
pub struct SrsSession {
    /// Recording session ID (from the SIPREC metadata).
    pub session_id: String,
    /// SIP Call-ID of the SIPREC INVITE dialog (SRC → SRS).
    pub recording_call_id: String,
    /// Original call ID being recorded (from metadata, if available).
    pub original_call_id: Option<String>,
    /// From-tag of the SIPREC INVITE.
    pub from_tag: String,
    /// To-tag generated by the SRS for the 200 OK.
    pub to_tag: String,
    /// Parsed recording metadata.
    pub metadata: RecordingMetadata,
    /// Current session state.
    pub state: SrsSessionState,
    /// When the session was created.
    pub started_at: Instant,
    /// When the session ended (set on BYE).
    pub ended_at: Option<Instant>,
    /// Directory where RTPEngine writes recording files.
    pub recording_dir: Option<PathBuf>,
}

impl SrsSession {
    /// Duration in seconds (returns 0 if not yet ended).
    pub fn duration_secs(&self) -> u64 {
        self.ended_at
            .map(|end| end.duration_since(self.started_at).as_secs())
            .unwrap_or(0)
    }

    /// Build a serializable summary for storage.
    pub fn to_recording_record(&self) -> RecordingRecord {
        RecordingRecord {
            session_id: self.session_id.clone(),
            recording_call_id: self.recording_call_id.clone(),
            original_call_id: self.original_call_id.clone(),
            participants: self.metadata.participants.iter().map(|participant| {
                ParticipantRecord {
                    participant_id: participant.participant_id.clone(),
                    aor: participant.aor.clone(),
                    name: participant.name.clone(),
                }
            }).collect(),
            streams: self.metadata.streams.iter().map(|stream| {
                StreamRecord {
                    stream_id: stream.stream_id.clone(),
                    label: stream.label.clone(),
                }
            }).collect(),
            state: self.state.to_string(),
            duration_secs: self.duration_secs(),
            recording_dir: self.recording_dir.as_ref().map(|path| path.display().to_string()),
        }
    }
}

// ---------------------------------------------------------------------------
// Serializable recording record
// ---------------------------------------------------------------------------

/// JSON-serializable recording metadata for storage backends.
#[derive(Debug, Clone, Serialize)]
pub struct RecordingRecord {
    pub session_id: String,
    pub recording_call_id: String,
    pub original_call_id: Option<String>,
    pub participants: Vec<ParticipantRecord>,
    pub streams: Vec<StreamRecord>,
    pub state: String,
    pub duration_secs: u64,
    pub recording_dir: Option<String>,
}

#[derive(Debug, Clone, Serialize)]
pub struct ParticipantRecord {
    pub participant_id: String,
    pub aor: String,
    pub name: Option<String>,
}

#[derive(Debug, Clone, Serialize)]
pub struct StreamRecord {
    pub stream_id: String,
    pub label: String,
}

// ---------------------------------------------------------------------------
// SRS manager
// ---------------------------------------------------------------------------

/// Manages all active SRS recording sessions.
#[derive(Debug)]
pub struct SrsManager {
    /// session_id → SrsSession
    sessions: DashMap<String, SrsSession>,
    /// recording_call_id → session_id (for routing BYE/re-INVITE by Call-ID)
    call_id_to_session: DashMap<String, String>,
    /// Configuration.
    config: SrsConfig,
}

impl SrsManager {
    /// Create a new SRS manager from config.
    pub fn new(config: SrsConfig) -> Self {
        Self {
            sessions: DashMap::new(),
            call_id_to_session: DashMap::new(),
            config,
        }
    }

    /// Register a new recording session from an inbound SIPREC INVITE.
    ///
    /// Returns the session ID and generated to-tag, or `None` if max sessions
    /// exceeded.
    pub fn create_session(
        &self,
        recording_call_id: &str,
        from_tag: &str,
        metadata: RecordingMetadata,
    ) -> Option<(String, String)> {
        // Check capacity.
        if self.sessions.len() >= self.config.max_sessions {
            warn!(
                max = self.config.max_sessions,
                current = self.sessions.len(),
                "SRS: max sessions reached, rejecting recording"
            );
            return None;
        }

        let session_id = metadata.session_id.clone();
        let to_tag = format!("srs-{}", &uuid::Uuid::new_v4().to_string()[..8]);

        // Build per-session recording directory.
        let recording_dir = PathBuf::from(&self.config.recording_dir).join(&session_id);

        let session = SrsSession {
            session_id: session_id.clone(),
            recording_call_id: recording_call_id.to_string(),
            original_call_id: metadata.sip_session_ids.first().cloned(),
            from_tag: from_tag.to_string(),
            to_tag: to_tag.clone(),
            metadata,
            state: SrsSessionState::Pending,
            started_at: Instant::now(),
            ended_at: None,
            recording_dir: Some(recording_dir),
        };

        info!(
            session_id = %session_id,
            recording_call_id = %recording_call_id,
            participants = session.metadata.participants.len(),
            streams = session.metadata.streams.len(),
            "SRS: new recording session"
        );

        self.sessions.insert(session_id.clone(), session);
        self.call_id_to_session.insert(recording_call_id.to_string(), session_id.clone());

        Some((session_id, to_tag))
    }

    /// Update an existing session from a re-INVITE (SDP/metadata refresh).
    ///
    /// Returns the existing to-tag so the 200 OK uses the same dialog tag,
    /// or `None` if the session doesn't exist.
    pub fn update_session(
        &self,
        recording_call_id: &str,
        metadata: RecordingMetadata,
    ) -> Option<String> {
        let session_id = self.call_id_to_session.get(recording_call_id)?
            .clone();

        if let Some(mut session) = self.sessions.get_mut(&session_id) {
            let to_tag = session.to_tag.clone();
            session.metadata = metadata;
            info!(
                session_id = %session_id,
                recording_call_id = %recording_call_id,
                participants = session.metadata.participants.len(),
                streams = session.metadata.streams.len(),
                "SRS: updated recording session (re-INVITE)"
            );
            Some(to_tag)
        } else {
            None
        }
    }

    /// Mark a session as actively recording (after RTPEngine setup succeeds).
    pub fn activate_session(&self, session_id: &str) {
        if let Some(mut session) = self.sessions.get_mut(session_id) {
            session.state = SrsSessionState::Recording;
            info!(session_id = %session_id, "SRS: recording active");
        }
    }

    /// Mark a session as failed.
    pub fn fail_session(&self, session_id: &str, reason: &str) {
        if let Some(mut session) = self.sessions.get_mut(session_id) {
            session.state = SrsSessionState::Failed(reason.to_string());
            session.ended_at = Some(Instant::now());
            warn!(session_id = %session_id, reason = %reason, "SRS: recording failed");
        }
    }

    /// Handle BYE from SRC — stop the recording session.
    ///
    /// Returns the recording record for storage, or `None` if not found.
    pub fn stop_session(&self, recording_call_id: &str) -> Option<RecordingRecord> {
        let session_id = self.call_id_to_session.get(recording_call_id)?
            .clone();

        let record = if let Some(mut session) = self.sessions.get_mut(&session_id) {
            session.state = SrsSessionState::Completed;
            session.ended_at = Some(Instant::now());
            let record = session.to_recording_record();
            info!(
                session_id = %session_id,
                duration = record.duration_secs,
                "SRS: recording completed"
            );
            Some(record)
        } else {
            None
        };

        // Clean up mappings.
        self.call_id_to_session.remove(recording_call_id);
        self.sessions.remove(&session_id);

        record
    }

    /// Look up a session by the SIPREC INVITE's Call-ID.
    pub fn session_for_call_id(&self, recording_call_id: &str) -> Option<String> {
        self.call_id_to_session.get(recording_call_id).map(|value| value.clone())
    }

    /// Check if a Call-ID belongs to an active SRS session.
    pub fn is_srs_session(&self, call_id: &str) -> bool {
        self.call_id_to_session.contains_key(call_id)
    }

    /// Number of active recording sessions.
    pub fn session_count(&self) -> usize {
        self.sessions.len()
    }

    /// Get the SRS config.
    pub fn config(&self) -> &SrsConfig {
        &self.config
    }

    /// Get session details by session ID.
    pub fn get_session(&self, session_id: &str) -> Option<dashmap::mapref::one::Ref<'_, String, SrsSession>> {
        self.sessions.get(session_id)
    }

    /// Get the recording directory for a session.
    pub fn recording_dir(&self, session_id: &str) -> Option<PathBuf> {
        self.sessions.get(session_id)
            .and_then(|session| session.recording_dir.clone())
    }

    /// Get the RTPEngine profile name to use for recording.
    pub fn rtpengine_profile(&self) -> &str {
        &self.config.rtpengine_profile
    }
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

#[cfg(test)]
mod tests {
    use super::*;
    use crate::siprec::metadata::{Participant, StreamInfo};

    fn test_config() -> SrsConfig {
        SrsConfig {
            enabled: true,
            recording_dir: "/tmp/siphon-test-recordings".to_string(),
            max_sessions: 100,
            backend: "file".to_string(),
            file: None,
            http: None,
            rtpengine_profile: "srs_recording".to_string(),
        }
    }

    fn test_metadata() -> RecordingMetadata {
        RecordingMetadata {
            session_id: "test-session-001".to_string(),
            sip_session_ids: vec![],
            participants: vec![
                Participant {
                    participant_id: "p1".to_string(),
                    aor: "sip:alice@example.com".to_string(),
                    name: None,
                },
                Participant {
                    participant_id: "p2".to_string(),
                    aor: "sip:bob@example.com".to_string(),
                    name: None,
                },
            ],
            streams: vec![
                StreamInfo {
                    stream_id: "s1".to_string(),
                    session_id: "test-session-001".to_string(),
                    label: "caller-audio".to_string(),
                },
            ],
        }
    }

    #[test]
    fn create_session_and_lookup() {
        let manager = SrsManager::new(test_config());
        let metadata = test_metadata();

        let result = manager.create_session("call-1", "from-tag-1", metadata);
        assert!(result.is_some());

        let (session_id, to_tag) = result.unwrap();
        assert_eq!(session_id, "test-session-001");
        assert!(to_tag.starts_with("srs-"));

        assert!(manager.is_srs_session("call-1"));
        assert!(!manager.is_srs_session("call-2"));
        assert_eq!(manager.session_count(), 1);
    }

    #[test]
    fn activate_session() {
        let manager = SrsManager::new(test_config());
        let metadata = test_metadata();

        let (session_id, _) = manager.create_session("call-1", "from-tag-1", metadata).unwrap();

        // Initially pending.
        let session = manager.get_session(&session_id).unwrap();
        assert_eq!(session.state, SrsSessionState::Pending);
        drop(session);

        // Activate.
        manager.activate_session(&session_id);
        let session = manager.get_session(&session_id).unwrap();
        assert_eq!(session.state, SrsSessionState::Recording);
    }

    #[test]
    fn stop_session_returns_record() {
        let manager = SrsManager::new(test_config());
        let metadata = test_metadata();

        let (session_id, _) = manager.create_session("call-1", "from-tag-1", metadata).unwrap();
        manager.activate_session(&session_id);

        let record = manager.stop_session("call-1");
        assert!(record.is_some());

        let record = record.unwrap();
        assert_eq!(record.session_id, "test-session-001");
        assert_eq!(record.participants.len(), 2);
        assert_eq!(record.state, "completed");

        // Session cleaned up.
        assert_eq!(manager.session_count(), 0);
        assert!(!manager.is_srs_session("call-1"));
    }

    #[test]
    fn fail_session() {
        let manager = SrsManager::new(test_config());
        let metadata = test_metadata();

        let (session_id, _) = manager.create_session("call-1", "from-tag-1", metadata).unwrap();
        manager.fail_session(&session_id, "RTPEngine unreachable");

        let session = manager.get_session(&session_id).unwrap();
        assert!(matches!(&session.state, SrsSessionState::Failed(reason) if reason == "RTPEngine unreachable"));
    }

    #[test]
    fn max_sessions_enforced() {
        let mut config = test_config();
        config.max_sessions = 2;
        let manager = SrsManager::new(config);

        // Create session 1.
        let metadata1 = RecordingMetadata {
            session_id: "sess-1".to_string(),
            sip_session_ids: vec![],
            participants: vec![],
            streams: vec![],
        };
        assert!(manager.create_session("call-1", "tag-1", metadata1).is_some());

        // Create session 2.
        let metadata2 = RecordingMetadata {
            session_id: "sess-2".to_string(),
            sip_session_ids: vec![],
            participants: vec![],
            streams: vec![],
        };
        assert!(manager.create_session("call-2", "tag-2", metadata2).is_some());

        // Session 3 should be rejected.
        let metadata3 = RecordingMetadata {
            session_id: "sess-3".to_string(),
            sip_session_ids: vec![],
            participants: vec![],
            streams: vec![],
        };
        assert!(manager.create_session("call-3", "tag-3", metadata3).is_none());
    }

    #[test]
    fn stop_nonexistent_session_returns_none() {
        let manager = SrsManager::new(test_config());
        assert!(manager.stop_session("no-such-call").is_none());
    }

    #[test]
    fn session_state_display() {
        assert_eq!(SrsSessionState::Pending.to_string(), "pending");
        assert_eq!(SrsSessionState::Recording.to_string(), "recording");
        assert_eq!(SrsSessionState::Stopping.to_string(), "stopping");
        assert_eq!(SrsSessionState::Completed.to_string(), "completed");
        assert_eq!(
            SrsSessionState::Failed("timeout".to_string()).to_string(),
            "failed: timeout"
        );
    }

    #[test]
    fn recording_record_serialization() {
        let manager = SrsManager::new(test_config());
        let metadata = test_metadata();

        let (session_id, _) = manager.create_session("call-1", "tag-1", metadata).unwrap();
        manager.activate_session(&session_id);

        let session = manager.get_session(&session_id).unwrap();
        let record = session.to_recording_record();

        let json = serde_json::to_string(&record).unwrap();
        assert!(json.contains("test-session-001"));
        assert!(json.contains("alice@example.com"));
        assert!(json.contains("caller-audio"));
    }

    #[test]
    fn recording_dir_per_session() {
        let manager = SrsManager::new(test_config());
        let metadata = test_metadata();

        let (session_id, _) = manager.create_session("call-1", "tag-1", metadata).unwrap();
        let dir = manager.recording_dir(&session_id).unwrap();
        assert_eq!(dir, PathBuf::from("/tmp/siphon-test-recordings/test-session-001"));
    }

    #[test]
    fn update_session_reinvite() {
        let manager = SrsManager::new(test_config());
        let metadata = test_metadata();

        let (_, original_to_tag) = manager.create_session("call-1", "from-tag-1", metadata).unwrap();
        manager.activate_session("test-session-001");

        // Simulate re-INVITE with updated metadata (e.g. new stream).
        let updated_metadata = RecordingMetadata {
            session_id: "test-session-001".to_string(),
            sip_session_ids: vec![],
            participants: vec![
                Participant {
                    participant_id: "p1".to_string(),
                    aor: "sip:alice@example.com".to_string(),
                    name: None,
                },
                Participant {
                    participant_id: "p2".to_string(),
                    aor: "sip:bob@example.com".to_string(),
                    name: None,
                },
            ],
            streams: vec![
                StreamInfo {
                    stream_id: "s1".to_string(),
                    session_id: "test-session-001".to_string(),
                    label: "caller-audio".to_string(),
                },
                StreamInfo {
                    stream_id: "s2".to_string(),
                    session_id: "test-session-001".to_string(),
                    label: "callee-audio".to_string(),
                },
            ],
        };

        let to_tag = manager.update_session("call-1", updated_metadata);
        assert!(to_tag.is_some());

        // To-tag must be the same as the original (same dialog).
        assert_eq!(to_tag.unwrap(), original_to_tag);

        // Session count must not increase.
        assert_eq!(manager.session_count(), 1);

        // Metadata must be updated.
        let session = manager.get_session("test-session-001").unwrap();
        assert_eq!(session.metadata.streams.len(), 2);

        // State must remain Recording (not reset to Pending).
        assert_eq!(session.state, SrsSessionState::Recording);
    }

    #[test]
    fn update_session_nonexistent_returns_none() {
        let manager = SrsManager::new(test_config());
        let metadata = test_metadata();

        let result = manager.update_session("no-such-call", metadata);
        assert!(result.is_none());
    }
}
