//! Session Description Protocol (SDP) types and utilities.
//!
//! This module provides types for working with SDP (Session Description Protocol),
//! which is used in WebRTC to negotiate media capabilities, codecs, network addresses,
//! and other session parameters between peers during the offer/answer exchange.
//!
//! # Overview
//!
//! SDP is a text-based protocol defined in [RFC 8866] that describes multimedia
//! sessions. In WebRTC, SDP is used to:
//!
//! - Describe available media codecs (audio/video formats)
//! - Exchange ICE candidates for NAT traversal
//! - Negotiate DTLS fingerprints for secure communication
//! - Establish media transport parameters
//! - Define simulcast and layered encoding configurations
//!
//! # Offer/Answer Model
//!
//! WebRTC uses the offer/answer model from [RFC 3264]:
//!
//! 1. **Offer**: The initiating peer creates an SDP offer describing their capabilities
//! 2. **Answer**: The remote peer responds with an SDP answer describing their chosen configuration
//! 3. **Pranswer**: Optional provisional answer before the final answer
//! 4. **Rollback**: Cancels negotiation and returns to the previous stable state
//!
//! # Public API
//!
//! This module exports two main types:
//!
//! - [`RTCSdpType`] - Enum representing the type of session description (Offer, Answer, etc.)
//! - [`RTCSessionDescription`] - Struct containing SDP text and its type
//!
//! # Examples
//!
//! ## Creating an Offer
//!
//! ```no_run
//! use rtc::peer_connection::sdp::{RTCSessionDescription, RTCSdpType};
//!
//! # fn example() -> Result<(), Box<dyn std::error::Error>> {
//! // SDP string generated by create_offer()
//! let sdp_string = "v=0\r\no=- 123 456 IN IP4 0.0.0.0\r\n...".to_string();
//!
//! // Wrap the SDP in an RTCSessionDescription
//! let offer = RTCSessionDescription::offer(sdp_string)?;
//!
//! assert_eq!(offer.sdp_type, RTCSdpType::Offer);
//! println!("Created offer: {}", offer);
//! # Ok(())
//! # }
//! ```
//!
//! ## Creating an Answer
//!
//! ```no_run
//! use rtc::peer_connection::sdp::{RTCSessionDescription, RTCSdpType};
//!
//! # fn example() -> Result<(), Box<dyn std::error::Error>> {
//! // SDP string generated by create_answer()
//! let sdp_string = "v=0\r\no=- 789 012 IN IP4 0.0.0.0\r\n...".to_string();
//!
//! // Wrap the SDP in an RTCSessionDescription
//! let answer = RTCSessionDescription::answer(sdp_string)?;
//!
//! assert_eq!(answer.sdp_type, RTCSdpType::Answer);
//! # Ok(())
//! # }
//! ```
//!
//! ## Using Provisional Answers
//!
//! ```no_run
//! use rtc::peer_connection::sdp::RTCSessionDescription;
//!
//! # fn example() -> Result<(), Box<dyn std::error::Error>> {
//! // Create a provisional answer that may be updated later
//! let sdp_string = "v=0\r\no=- 345 678 IN IP4 0.0.0.0\r\n...".to_string();
//! let pranswer = RTCSessionDescription::pranswer(sdp_string)?;
//!
//! // Later, you can send a final answer to complete negotiation
//! # Ok(())
//! # }
//! ```
//!
//! ## Parsing SDP Content
//!
//! ```no_run
//! use rtc::peer_connection::sdp::RTCSessionDescription;
//!
//! # fn example() -> Result<(), Box<dyn std::error::Error>> {
//! let offer = RTCSessionDescription::offer(
//!     "v=0\r\no=- 123 456 IN IP4 0.0.0.0\r\n...".to_string()
//! )?;
//!
//! // Unmarshal parses the SDP text into a structured format
//! let parsed = offer.unmarshal()?;
//! println!("Session name: {}", parsed.session_name);
//! # Ok(())
//! # }
//! ```
//!
//! ## Serializing to JSON
//!
//! ```no_run
//! use rtc::peer_connection::sdp::{RTCSessionDescription, RTCSdpType};
//!
//! # fn example() -> Result<(), Box<dyn std::error::Error>> {
//! // Create an offer description (parsed field set automatically)
//! let desc = RTCSessionDescription::offer(
//!     "v=0\r\no=- 123 456 IN IP4 0.0.0.0\r\ns=-\r\nt=0 0\r\n".to_string()
//! )?;
//!
//! // Serialize for transmission to remote peer
//! let json = serde_json::to_string(&desc)?;
//! println!("JSON: {}", json);
//!
//! // Deserialize received SDP
//! let received: RTCSessionDescription = serde_json::from_str(&json)?;
//! assert_eq!(received.sdp_type, RTCSdpType::Offer);
//! # Ok(())
//! # }
//! ```
//!
//! # SDP Structure
//!
//! A typical WebRTC SDP contains:
//!
//! - **Session-level attributes**: Version, origin, timing
//! - **Media sections**: One per audio/video track or data channel
//! - **ICE candidates**: Network addresses for connectivity
//! - **Codec information**: Supported formats and parameters
//! - **DTLS fingerprints**: For secure communication
//!
//! # Specifications
//!
//! - [RFC 8866] - SDP: Session Description Protocol
//! - [RFC 3264] - An Offer/Answer Model with SDP
//! - [RFC 8829] - JavaScript Session Establishment Protocol (JSEP)
//! - [W3C WebRTC Specification]
//!
//! [RFC 8866]: https://datatracker.ietf.org/doc/html/rfc8866
//! [RFC 3264]: https://datatracker.ietf.org/doc/html/rfc3264
//! [RFC 8829]: https://datatracker.ietf.org/doc/html/rfc8829
//! [W3C WebRTC Specification]: https://w3c.github.io/webrtc-pc/#session-description-model

//TODO: #[cfg(test)]
//TODO: mod sdp_test;

pub(crate) mod sdp_type;
pub(crate) mod session_description;

pub use sdp_type::RTCSdpType;
pub use session_description::RTCSessionDescription;

use crate::media_stream::MediaStreamId;
use crate::media_stream::track::MediaStreamTrackId;
use crate::peer_connection::RTCPeerConnection;
use crate::peer_connection::configuration::media_engine::MediaEngine;
use crate::peer_connection::state::ice_gathering_state::RTCIceGatheringState;
use crate::peer_connection::transport::dtls::fingerprint::RTCDtlsFingerprint;
use crate::peer_connection::transport::ice::candidate::RTCIceCandidate;
use crate::peer_connection::transport::ice::parameters::RTCIceParameters;
use crate::rtp_transceiver::direction::RTCRtpTransceiverDirection;
use crate::rtp_transceiver::rtp_sender::rtp_codec::{RTCRtpCodec, RtpCodecKind};
use crate::rtp_transceiver::rtp_sender::rtp_codec_parameters::RTCRtpCodecParameters;
use crate::rtp_transceiver::{
    PayloadType, RtpStreamId, SSRC, internal::RTCRtpTransceiverInternal,
    rtp_sender::rtcp_parameters::RTCPFeedback,
};
use ice::candidate::{Candidate, unmarshal_candidate};
use interceptor::Interceptor;
use sdp::description::common::{Address, ConnectionInformation};
use sdp::description::media::*;
use sdp::description::session::*;
use sdp::extmap::ExtMap;
use sdp::util::ConnectionRole;
use shared::error::{Error, Result};
use std::collections::HashMap;
use std::io::BufReader;
use std::str::FromStr;
use url::Url;

pub(crate) const SDP_ATTRIBUTE_RID: &str = "rid";
pub(crate) const SDP_ATTRIBUTE_SIMULCAST: &str = "simulcast";
pub(crate) const GENERATED_CERTIFICATE_ORIGIN: &str = "WebRTC";
pub(crate) const MEDIA_SECTION_APPLICATION: &str = "application";

/// TrackDetails represents any media source that can be represented in a SDP
/// This isn't keyed by SSRC because it also needs to support rid based sources
#[derive(Default, Debug, Clone)]
pub(crate) struct TrackDetails {
    pub(crate) mid: String,
    pub(crate) kind: RtpCodecKind,
    pub(crate) stream_id: MediaStreamId,
    pub(crate) track_id: MediaStreamTrackId,
    pub(crate) ssrc: Option<SSRC>,
    pub(crate) rtx_ssrc: Option<SSRC>,
    pub(crate) fec_ssrc: Option<SSRC>,
    pub(crate) rids: Vec<String>,
}

pub(crate) fn track_details_for_ssrc(
    track_details: &[TrackDetails],
    ssrc: SSRC,
) -> Option<&TrackDetails> {
    track_details.iter().find(|x| x.ssrc == Some(ssrc))
}

pub(crate) fn track_details_for_rid<'a>(
    track_details: &'a [TrackDetails],
    mid: &String,
    rid: &String,
) -> Option<&'a TrackDetails> {
    track_details
        .iter()
        .find(|x| &x.mid == mid && x.rids.contains(rid))
}

pub(crate) fn filter_track_with_ssrc(incoming_tracks: &mut Vec<TrackDetails>, ssrc: SSRC) {
    incoming_tracks.retain(|x| x.ssrc != Some(ssrc));
}

/// extract all TrackDetails from an SDP.
pub(crate) fn track_details_from_sdp(s: &SessionDescription) -> Vec<TrackDetails> {
    let mut incoming_tracks = vec![];

    for media in &s.media_descriptions {
        let mut tracks_in_media_section = vec![];
        let mut rtx_repair_flows = HashMap::new();
        let mut fec_repair_flows = HashMap::new();

        let mut stream_id = "";
        let mut track_id = "";

        // If media section is recvonly or inactive skip
        if media.attribute(ATTR_KEY_RECV_ONLY).is_some()
            || media.attribute(ATTR_KEY_INACTIVE).is_some()
        {
            continue;
        }

        let mid_value = match get_mid_value(media) {
            Some(mid_value) if !mid_value.is_empty() => mid_value,
            _ => continue,
        };

        let codec_type = RtpCodecKind::from(media.media_name.media.as_str());
        if codec_type == RtpCodecKind::Unspecified {
            continue;
        }

        for attr in &media.attributes {
            match attr.key.as_str() {
                ATTR_KEY_SSRC_GROUP => {
                    if let Some(value) = &attr.value {
                        let split: Vec<&str> = value.split(' ').collect();
                        if split[0] == SEMANTIC_TOKEN_FLOW_IDENTIFICATION {
                            // Add rtx ssrcs to blacklist, to avoid adding them as tracks
                            // Essentially lines like `a=ssrc-group:FID 2231627014 632943048` are processed by this section
                            // as this declares that the second SSRC (632943048) is a rtx repair flow (RFC4588) for the first
                            // (2231627014) as specified in RFC5576
                            if split.len() == 3 {
                                let base_ssrc = match split[1].parse::<u32>() {
                                    Ok(ssrc) => ssrc,
                                    Err(err) => {
                                        log::warn!("Failed to parse SSRC: {err}");
                                        continue;
                                    }
                                };
                                let rtx_repair_flow = match split[2].parse::<u32>() {
                                    Ok(n) => n,
                                    Err(err) => {
                                        log::warn!("Failed to parse SSRC: {err}");
                                        continue;
                                    }
                                };
                                rtx_repair_flows.insert(rtx_repair_flow, base_ssrc);
                                // Remove if rtx was added as track before
                                filter_track_with_ssrc(
                                    &mut tracks_in_media_section,
                                    rtx_repair_flow as SSRC,
                                );

                                tracks_in_media_section.iter_mut().for_each(|x| {
                                    if x.ssrc.is_some_and(|ssrc| ssrc == base_ssrc) {
                                        x.rtx_ssrc = Some(rtx_repair_flow);
                                    }
                                });
                            }
                        } else if split[0] == SEMANTIC_TOKEN_FORWARD_ERROR_CORRECTION_FRAMEWORK {
                            // Similar to above, lines like `a=ssrc-group:FEC-FR aaaaa bbbbb`
                            // means for video ssrc aaaaa, there's a FEC track bbbbb
                            if split.len() == 3 {
                                let base_ssrc = match split[1].parse::<u32>() {
                                    Ok(ssrc) => ssrc,
                                    Err(err) => {
                                        log::warn!("Failed to parse SSRC: {err}");
                                        continue;
                                    }
                                };
                                let fec_repair_flow = match split[2].parse::<u32>() {
                                    Ok(n) => n,
                                    Err(err) => {
                                        log::warn!("Failed to parse SSRC: {err}");
                                        continue;
                                    }
                                };
                                fec_repair_flows.insert(fec_repair_flow, base_ssrc);
                                // Remove if fec was added as track before
                                filter_track_with_ssrc(
                                    &mut tracks_in_media_section,
                                    fec_repair_flow as SSRC,
                                );

                                tracks_in_media_section.iter_mut().for_each(|x| {
                                    if x.ssrc.is_some_and(|ssrc| ssrc == base_ssrc) {
                                        x.fec_ssrc = Some(fec_repair_flow);
                                    }
                                });
                            }
                        }
                    }
                }

                // Handle `a=msid:<stream_id> <track_label>` The first value is the same as MediaStream.id
                // in the browser and can be used to figure out which tracks belong to the same stream. The browser should
                // figure this out automatically when an ontrack event is emitted on RTCPeerConnection.
                ATTR_KEY_MSID => {
                    if let Some(value) = &attr.value {
                        let mut split = value.split(' ');

                        if let (Some(sid), Some(tid), None) =
                            (split.next(), split.next(), split.next())
                        {
                            stream_id = sid;
                            track_id = tid;
                        }
                    }
                }

                ATTR_KEY_SSRC => {
                    if let Some(value) = &attr.value {
                        let split: Vec<&str> = value.split(' ').collect();
                        let ssrc = match split[0].parse::<u32>() {
                            Ok(ssrc) => ssrc,
                            Err(err) => {
                                log::warn!("Failed to parse SSRC: {err}");
                                continue;
                            }
                        };

                        if rtx_repair_flows.contains_key(&ssrc) {
                            continue; // This ssrc is a RTX repair flow, ignore
                        }
                        if fec_repair_flows.contains_key(&ssrc) {
                            continue; // This ssrc is a FEC repair flow, ignore
                        }

                        if split.len() == 3 && split[1].starts_with("msid:") {
                            stream_id = &split[1]["msid:".len()..];
                            track_id = split[2];
                        }

                        let mut track_idx = tracks_in_media_section.len();
                        for (i, t) in tracks_in_media_section.iter().enumerate() {
                            if t.ssrc == Some(ssrc) {
                                track_idx = i;
                                break;
                            }
                        }

                        if track_idx < tracks_in_media_section.len() {
                            tracks_in_media_section[track_idx].mid = mid_value.to_string();
                            tracks_in_media_section[track_idx].kind = codec_type;
                            stream_id.clone_into(&mut tracks_in_media_section[track_idx].stream_id);
                            track_id.clone_into(&mut tracks_in_media_section[track_idx].track_id);
                            tracks_in_media_section[track_idx].ssrc = Some(ssrc);
                        } else {
                            let track_details = TrackDetails {
                                mid: mid_value.to_string(),
                                kind: codec_type,
                                stream_id: stream_id.to_owned(),
                                track_id: track_id.to_owned(),
                                ssrc: Some(ssrc),
                                ..Default::default()
                            };
                            tracks_in_media_section.push(track_details);
                        }
                    }
                }
                _ => {}
            };
        }

        for (repair_ssrc, base_ssrc) in rtx_repair_flows {
            for track in &mut tracks_in_media_section {
                if track.ssrc == Some(base_ssrc) {
                    track.rtx_ssrc = Some(repair_ssrc);
                }
            }
        }

        for (repair_ssrc, base_ssrc) in fec_repair_flows {
            for track in &mut tracks_in_media_section {
                if track.ssrc == Some(base_ssrc) {
                    track.fec_ssrc = Some(repair_ssrc);
                }
            }
        }

        // If media line is using RTP Stream Identifier Source Description per RFC8851
        // we will need to override tracks, and remove ssrcs.
        // This is in particular important for Firefox, as it uses both 'rid', 'simulcast'
        // and 'a=ssrc' lines.
        if !track_id.is_empty() && !stream_id.is_empty() {
            let rids = get_rids(media);
            if !rids.is_empty() {
                let simulcast_track = TrackDetails {
                    mid: mid_value.to_string(),
                    kind: codec_type,
                    stream_id: stream_id.to_owned(),
                    track_id: track_id.to_owned(),
                    rids: rids.iter().map(|r| r.id.clone()).collect(),
                    ..Default::default()
                };

                tracks_in_media_section = vec![simulcast_track];
            } else if tracks_in_media_section.is_empty() {
                // no rids, no ssrc set
                let implicit_track = TrackDetails {
                    mid: mid_value.to_string(),
                    kind: codec_type,
                    stream_id: stream_id.to_owned(),
                    track_id: track_id.to_owned(),
                    ..Default::default()
                };

                tracks_in_media_section = vec![implicit_track];
            }
        }

        incoming_tracks.extend(tracks_in_media_section);
    }

    incoming_tracks
}

pub(crate) fn get_rids(media: &MediaDescription) -> Vec<SimulcastRid> {
    let mut rids = vec![];
    let mut simulcast_attr: Option<String> = None;
    for attr in &media.attributes {
        if attr.key.as_str() == SDP_ATTRIBUTE_RID {
            if let Err(err) = attr
                .value
                .as_ref()
                .ok_or(Error::SimulcastRidParseErrorSyntaxIdDirSplit)
                .and_then(SimulcastRid::try_from)
                .map(|rid| rids.push(rid))
            {
                log::warn!("Failed to parse RID: {err}");
            }
        } else if attr.key.as_str() == SDP_ATTRIBUTE_SIMULCAST {
            simulcast_attr.clone_from(&attr.value);
        }
    }

    if let Some(attr) = simulcast_attr {
        let mut split = attr.split(' ');
        loop {
            let _dir = split.next();
            let sc_str_list = split.next();
            if let Some(list) = sc_str_list {
                let sc_list: Vec<&str> = list.split(';').flat_map(|alt| alt.split(',')).collect();
                for sc_id in sc_list {
                    let (sc_id, paused) = if let Some(sc_id) = sc_id.strip_prefix('~') {
                        (sc_id, true)
                    } else {
                        (sc_id, false)
                    };

                    if let Some(rid) = rids.iter_mut().find(|f| f.id == sc_id) {
                        rid.paused = paused;
                    }
                }
            } else {
                break;
            }
        }
    }

    rids
}

pub(crate) fn add_candidates_to_media_descriptions(
    candidates: &[RTCIceCandidate],
    mut m: MediaDescription,
    ice_gathering_state: RTCIceGatheringState,
) -> Result<MediaDescription> {
    let append_candidate_if_new = |c: &Candidate, m: MediaDescription| -> MediaDescription {
        let marshaled = c.marshal();
        for a in &m.attributes {
            if let Some(value) = &a.value
                && &marshaled == value
            {
                return m;
            }
        }

        m.with_value_attribute("candidate".to_owned(), marshaled)
    };

    for c in candidates {
        let mut candidate = c.to_ice()?;

        candidate.set_component(1);
        m = append_candidate_if_new(&candidate, m);

        candidate.set_component(2);
        m = append_candidate_if_new(&candidate, m);
    }

    if ice_gathering_state != RTCIceGatheringState::Complete {
        return Ok(m);
    }
    for a in &m.attributes {
        if &a.key == "end-of-candidates" {
            return Ok(m);
        }
    }

    Ok(m.with_property_attribute("end-of-candidates".to_owned()))
}

pub(crate) struct AddDataMediaSectionParams {
    should_add_candidates: bool,
    mid_value: String,
    ice_params: RTCIceParameters,
    dtls_role: ConnectionRole,
    ice_gathering_state: RTCIceGatheringState,
    sctp_max_message_size: usize,
}

pub(crate) fn add_data_media_section(
    d: SessionDescription,
    dtls_fingerprints: &[RTCDtlsFingerprint],
    candidates: &[RTCIceCandidate],
    params: AddDataMediaSectionParams,
) -> Result<SessionDescription> {
    let mut media = MediaDescription {
        media_name: MediaName {
            media: MEDIA_SECTION_APPLICATION.to_owned(),
            port: RangedPort {
                value: 9,
                range: None,
            },
            protos: vec!["UDP".to_owned(), "DTLS".to_owned(), "SCTP".to_owned()],
            formats: vec!["webrtc-datachannel".to_owned()],
        },
        media_title: None,
        connection_information: Some(ConnectionInformation {
            network_type: "IN".to_owned(),
            address_type: "IP4".to_owned(),
            address: Some(Address {
                address: "0.0.0.0".to_owned(),
                ttl: None,
                range: None,
            }),
        }),
        bandwidth: vec![],
        encryption_key: None,
        attributes: vec![],
    }
    .with_value_attribute(
        ATTR_KEY_CONNECTION_SETUP.to_owned(),
        params.dtls_role.to_string(),
    )
    .with_value_attribute(ATTR_KEY_MID.to_owned(), params.mid_value)
    .with_property_attribute(RTCRtpTransceiverDirection::Sendrecv.to_string())
    .with_property_attribute("sctp-port:5000".to_owned())
    .with_value_attribute(
        "max-message-size".to_owned(),
        format!("{}", params.sctp_max_message_size),
    )
    .with_ice_credentials(
        params.ice_params.username_fragment,
        params.ice_params.password,
    );

    for f in dtls_fingerprints {
        media = media.with_fingerprint(f.algorithm.clone(), f.value.to_uppercase());
    }

    if params.should_add_candidates {
        media =
            add_candidates_to_media_descriptions(candidates, media, params.ice_gathering_state)?;
    }

    Ok(d.with_media(media))
}

pub(crate) struct AddTransceiverSdpParams {
    should_add_candidates: bool,
    mid_value: String,
    dtls_role: ConnectionRole,
    ice_gathering_state: RTCIceGatheringState,
    ignore_rid_pause_for_recv: bool,
    write_ssrc_attributes_for_simulcast: bool,
}

impl<I> RTCPeerConnection<I>
where
    I: Interceptor,
{
    #[allow(clippy::too_many_arguments)]
    pub(crate) fn add_transceiver_sdp(
        mut d: SessionDescription,
        dtls_fingerprints: &[RTCDtlsFingerprint],
        media_engine: &MediaEngine,
        transceivers: &mut [RTCRtpTransceiverInternal<I>],
        ice_params: &RTCIceParameters,
        candidates: &[RTCIceCandidate],
        media_section: &MediaSection,
        params: AddTransceiverSdpParams,
    ) -> Result<(SessionDescription, bool)> {
        let (
            should_add_candidates,
            mid_value,
            dtls_role,
            ice_gathering_state,
            ignore_rid_pause_for_recv,
            write_ssrc_attributes_for_simulcast,
        ) = (
            params.should_add_candidates,
            params.mid_value,
            params.dtls_role,
            params.ice_gathering_state,
            params.ignore_rid_pause_for_recv,
            params.write_ssrc_attributes_for_simulcast,
        );

        let transceiver = &mut transceivers[media_section.transceiver_index];
        let mut media =
            MediaDescription::new_jsep_media_description(transceiver.kind().to_string(), vec![])
                .with_value_attribute(ATTR_KEY_CONNECTION_SETUP.to_owned(), dtls_role.to_string())
                .with_value_attribute(ATTR_KEY_MID.to_owned(), mid_value.clone())
                .with_ice_credentials(
                    ice_params.username_fragment.clone(),
                    ice_params.password.clone(),
                )
                .with_property_attribute(ATTR_KEY_RTCPMUX.to_owned())
                .with_property_attribute(ATTR_KEY_RTCPRSIZE.to_owned());

        let codecs = transceiver.get_codecs(media_engine);
        for codec in &codecs {
            let name = codec
                .rtp_codec
                .mime_type
                .trim_start_matches("audio/")
                .trim_start_matches("video/")
                .to_owned();
            media = media.with_codec(
                codec.payload_type,
                name,
                codec.rtp_codec.clock_rate,
                codec.rtp_codec.channels,
                codec.rtp_codec.sdp_fmtp_line.clone(),
            );

            for feedback in &codec.rtp_codec.rtcp_feedback {
                media = media.with_value_attribute(
                    "rtcp-fb".to_owned(),
                    if feedback.parameter.is_empty() {
                        format!("{} {}", codec.payload_type, feedback.typ)
                    } else {
                        format!(
                            "{} {} {}",
                            codec.payload_type, feedback.typ, feedback.parameter
                        )
                    },
                );
            }
        }
        if codecs.is_empty() {
            // If we are sender and have no codecs throw an error early
            if transceiver.sender().is_some() {
                return Err(Error::ErrSenderWithNoCodecs);
            }

            // Explicitly reject track if we don't have the codec
            // RFC 8829 Section 5.3.1: Set port=0 to reject, further processing can be skipped
            // However, mid attribute is still needed for offer/answer correlation with BUNDLE
            d = d.with_media(
                MediaDescription {
                    media_name: MediaName {
                        media: transceiver.kind().to_string(),
                        port: RangedPort {
                            value: 0,
                            range: None,
                        },
                        protos: vec![
                            "UDP".to_owned(),
                            "TLS".to_owned(),
                            "RTP".to_owned(),
                            "SAVPF".to_owned(),
                        ],
                        formats: vec!["0".to_owned()],
                    },
                    media_title: None,
                    // We need to include connection information even if we're rejecting a track, otherwise Firefox will fail to
                    // parse the SDP with an error like:
                    // SIPCC Failed to parse SDP: SDP Parse Error on line 50:  c= connection line not specified for every media level, validation failed.
                    // In addition this makes our SDP compliant with RFC 4566 Section 5.7: https://datatracker.ietf.org/doc/html/rfc4566#section-5.7
                    connection_information: Some(ConnectionInformation {
                        network_type: "IN".to_owned(),
                        address_type: "IP4".to_owned(),
                        address: Some(Address {
                            address: "0.0.0.0".to_owned(),
                            ttl: None,
                            range: None,
                        }),
                    }),
                    bandwidth: vec![],
                    encryption_key: None,
                    attributes: vec![],
                }
                .with_value_attribute(ATTR_KEY_MID.to_owned(), mid_value),
            );
            return Ok((d, false));
        }

        let parameters =
            media_engine.get_rtp_parameters_by_kind(transceiver.kind(), transceiver.direction());
        for rtp_extension in &parameters.header_extensions {
            if !media_section.match_extensions.is_empty()
                && !media_section
                    .match_extensions
                    .contains_key(&rtp_extension.uri)
            {
                continue;
            }

            let ext_url = Url::parse(rtp_extension.uri.as_str())?;
            media = media.with_extmap(sdp::extmap::ExtMap {
                value: rtp_extension.id,
                uri: Some(ext_url),
                ..Default::default()
            });
        }

        let mut recv_sc_list: Vec<String> = vec![];
        let mut send_sc_list: Vec<String> = vec![];
        if !media_section.rid_map.is_empty() {
            for rid in &media_section.rid_map {
                let rid_syntax = match rid.direction {
                    SimulcastDirection::Send => {
                        // If Send rid, then reply with a recv rid
                        if rid.paused {
                            recv_sc_list.push(format!("~{}", rid.id));
                        } else {
                            recv_sc_list.push(rid.id.to_owned());
                        }
                        format!("{} recv", rid.id)
                    }
                    SimulcastDirection::Recv => {
                        // If Recv rid, then reply with a send rid
                        if rid.paused && !ignore_rid_pause_for_recv {
                            send_sc_list.push(format!("~{}", rid.id));
                        } else {
                            send_sc_list.push(rid.id.to_owned());
                        }
                        format!("{} send", rid.id)
                    }
                };
                media = media.with_value_attribute(SDP_ATTRIBUTE_RID.to_owned(), rid_syntax);
            }
        }

        media = {
            let (media, send_rids) = RTCPeerConnection::add_sender_sdp(
                media,
                media_engine,
                transceiver,
                write_ssrc_attributes_for_simulcast,
            );

            // Simulcast
            let mut sc_attr = vec![];
            if !recv_sc_list.is_empty() {
                sc_attr.push(format!("recv {}", recv_sc_list.join(";")));
            }
            if !send_sc_list.is_empty() {
                sc_attr.push(format!("send {}", send_sc_list.join(";")));
            }
            if !send_rids.is_empty() {
                sc_attr.push(format!("send {}", send_rids.join(";")));
            }
            if !sc_attr.is_empty() {
                media.with_value_attribute(SDP_ATTRIBUTE_SIMULCAST.to_owned(), sc_attr.join(" "))
            } else {
                media
            }
        };

        media = media.with_property_attribute(transceiver.direction().to_string());

        for fingerprint in dtls_fingerprints {
            media = media.with_fingerprint(
                fingerprint.algorithm.to_owned(),
                fingerprint.value.to_uppercase(),
            );
        }

        if should_add_candidates {
            media = add_candidates_to_media_descriptions(candidates, media, ice_gathering_state)?;
        }

        Ok((d.with_media(media), true))
    }

    fn add_sender_sdp(
        mut media: MediaDescription,
        media_engine: &MediaEngine,
        transceiver: &mut RTCRtpTransceiverInternal<I>,
        write_ssrc_attributes_for_simulcast: bool,
    ) -> (MediaDescription, Vec<RtpStreamId>) {
        let mut send_rids = vec![];

        if let Some(sender) = transceiver.sender_mut() {
            let (encodings, track) = (
                sender.get_parameters(media_engine).encodings.clone(),
                sender.track(),
            );

            let is_simulcast = encodings.len() > 1
                && encodings
                    .iter()
                    .all(|encoding| !encoding.rtp_coding_parameters.rid.is_empty());

            media = media.with_property_attribute(format!(
                "msid:{} {}",
                track.stream_id(),
                track.track_id()
            ));

            if write_ssrc_attributes_for_simulcast || !is_simulcast {
                for encoding in &encodings {
                    if let Some(&ssrc) = encoding.rtp_coding_parameters.ssrc.as_ref() {
                        if let Some(rtx) = encoding.rtp_coding_parameters.rtx.as_ref() {
                            media = media.with_value_attribute(
                                ATTR_KEY_SSRC_GROUP.to_owned(),
                                format!(
                                    "{} {} {}",
                                    SEMANTIC_TOKEN_FLOW_IDENTIFICATION, ssrc, rtx.ssrc
                                ),
                            );
                        }

                        if let Some(fec) = encoding.rtp_coding_parameters.fec.as_ref() {
                            media = media.with_value_attribute(
                                ATTR_KEY_SSRC_GROUP.to_owned(),
                                format!(
                                    "{} {} {}",
                                    SEMANTIC_TOKEN_FORWARD_ERROR_CORRECTION, ssrc, fec.ssrc
                                ),
                            );
                        }

                        media = media.with_media_source(
                            ssrc,
                            track.stream_id().clone(),    /* cname */
                            track.stream_id().to_owned(), /* stream_id */
                            track.track_id().to_owned(),
                        );

                        if let Some(rtx) = encoding.rtp_coding_parameters.rtx.as_ref() {
                            media = media.with_media_source(
                                rtx.ssrc,
                                track.stream_id().clone(),    /* cname */
                                track.stream_id().to_owned(), /* stream_id */
                                track.track_id().to_owned(),
                            );
                        }

                        if let Some(fec) = encoding.rtp_coding_parameters.fec.as_ref() {
                            media = media.with_media_source(
                                fec.ssrc,
                                track.stream_id().clone(),    /* cname */
                                track.stream_id().to_owned(), /* stream_id */
                                track.track_id().to_owned(),
                            );
                        }
                    }
                }
            }

            if is_simulcast {
                for encoding in &encodings {
                    media = media.with_value_attribute(
                        SDP_ATTRIBUTE_RID.to_owned(),
                        format!("{} send", encoding.rtp_coding_parameters.rid),
                    );
                    send_rids.push(encoding.rtp_coding_parameters.rid.to_string());
                }
            }
        }

        (media, send_rids)
    }
}

#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub(crate) enum SimulcastDirection {
    Send,
    Recv,
}

impl TryFrom<&str> for SimulcastDirection {
    type Error = Error;
    fn try_from(value: &str) -> std::result::Result<Self, Self::Error> {
        match value.to_lowercase().as_str() {
            "send" => Ok(SimulcastDirection::Send),
            "recv" => Ok(SimulcastDirection::Recv),
            _ => Err(Error::SimulcastRidParseErrorUnknownDirection),
        }
    }
}

#[derive(Clone, Debug)]
pub(crate) struct SimulcastRid {
    pub(crate) id: String,
    pub(crate) direction: SimulcastDirection,
    pub(crate) params: String,
    pub(crate) paused: bool,
}

impl TryFrom<&String> for SimulcastRid {
    type Error = Error;
    fn try_from(value: &String) -> std::result::Result<Self, Self::Error> {
        let mut split = value.split(' ');
        let id = split
            .next()
            .ok_or(Error::SimulcastRidParseErrorSyntaxIdDirSplit)?
            .to_owned();
        let direction = SimulcastDirection::try_from(
            split
                .next()
                .ok_or(Error::SimulcastRidParseErrorSyntaxIdDirSplit)?,
        )?;
        let params = split.collect();

        Ok(Self {
            id,
            direction,
            params,
            paused: false,
        })
    }
}

fn bundle_match(bundle: Option<&String>, id: &str) -> bool {
    match bundle {
        None => true,
        Some(b) => b.split_whitespace().any(|s| s == id),
    }
}

#[derive(Default)]
pub(crate) struct MediaSection {
    pub(crate) mid: String,
    pub(crate) transceiver_index: usize,
    pub(crate) data: bool,
    pub(crate) match_extensions: HashMap<String, u16>,
    pub(crate) rid_map: Vec<SimulcastRid>,
}

pub(crate) struct PopulateSdpParams {
    pub(crate) media_description_fingerprint: bool,
    pub(crate) is_ice_lite: bool,
    pub(crate) is_extmap_allow_mixed: bool,
    pub(crate) connection_role: ConnectionRole,
    pub(crate) ice_gathering_state: RTCIceGatheringState,
    pub(crate) match_bundle_group: Option<String>,
    pub(crate) sctp_max_message_size: usize,
    pub(crate) ignore_rid_pause_for_recv: bool,
    pub(crate) write_ssrc_attributes_for_simulcast: bool,
}

impl<I> RTCPeerConnection<I>
where
    I: Interceptor,
{
    pub(crate) fn find_by_mid(
        mid: &String,
        local_transceivers: &[RTCRtpTransceiverInternal<I>],
    ) -> Option<usize> {
        local_transceivers
            .iter()
            .enumerate()
            .find(|(_i, t)| t.mid().as_ref() == Some(mid))
            .map(|(i, _v)| i)
    }

    /// Given a direction+type pluck a transceiver from the passed list
    /// if no entry satisfies the requested type+direction return a inactive Transceiver
    pub(crate) fn satisfy_type_and_direction(
        remote_kind: RtpCodecKind,
        remote_direction: RTCRtpTransceiverDirection,
        local_transceivers: &mut [RTCRtpTransceiverInternal<I>],
    ) -> Option<&mut RTCRtpTransceiverInternal<I>> {
        // Get direction order from most preferred to least
        let get_preferred_directions = || -> Vec<RTCRtpTransceiverDirection> {
            match remote_direction {
                RTCRtpTransceiverDirection::Sendrecv => vec![
                    RTCRtpTransceiverDirection::Recvonly,
                    RTCRtpTransceiverDirection::Sendrecv,
                ],
                RTCRtpTransceiverDirection::Sendonly => vec![RTCRtpTransceiverDirection::Recvonly],
                RTCRtpTransceiverDirection::Recvonly => vec![
                    RTCRtpTransceiverDirection::Sendonly,
                    RTCRtpTransceiverDirection::Sendrecv,
                ],
                _ => vec![],
            }
        };

        for possible_direction in get_preferred_directions() {
            // Find the index first to avoid multiple mutable borrows
            if let Some(index) = local_transceivers.iter().position(|t| {
                t.mid().is_none() && t.kind() == remote_kind && possible_direction == t.direction()
            }) {
                return Some(&mut local_transceivers[index]);
            }
        }

        None
    }

    /// populate_sdp serializes a PeerConnections state into an SDP
    #[allow(clippy::too_many_arguments)]
    pub(crate) fn populate_sdp(
        mut d: SessionDescription,
        dtls_fingerprints: &[RTCDtlsFingerprint],
        media_engine: &MediaEngine,
        transceivers: &mut [RTCRtpTransceiverInternal<I>],
        candidates: &[RTCIceCandidate],
        ice_params: &RTCIceParameters,
        media_sections: &[MediaSection],
        params: PopulateSdpParams,
    ) -> Result<SessionDescription> {
        let media_dtls_fingerprints = if params.media_description_fingerprint {
            dtls_fingerprints.to_vec()
        } else {
            vec![]
        };

        let mut bundle_value = "BUNDLE".to_owned();
        let mut bundle_count = 0;
        let append_bundle = |mid_value: &str, value: &mut String, count: &mut i32| {
            *value = value.clone() + " " + mid_value;
            *count += 1;
        };

        for (i, m) in media_sections.iter().enumerate() {
            if m.data && m.transceiver_index != usize::MAX {
                return Err(Error::ErrSDPMediaSectionMediaDataChanInvalid);
            } else if !m.data && m.transceiver_index >= transceivers.len() {
                return Err(Error::ErrSDPMediaSectionTrackInvalid);
            }

            let should_add_candidates = i == 0;

            let should_add_id = if m.data {
                let params = AddDataMediaSectionParams {
                    should_add_candidates,
                    mid_value: m.mid.clone(),
                    ice_params: ice_params.clone(),
                    dtls_role: params.connection_role,
                    ice_gathering_state: params.ice_gathering_state,
                    sctp_max_message_size: params.sctp_max_message_size,
                };
                d = add_data_media_section(d, &media_dtls_fingerprints, candidates, params)?;
                true
            } else {
                let params = AddTransceiverSdpParams {
                    should_add_candidates,
                    mid_value: m.mid.clone(),
                    dtls_role: params.connection_role,
                    ice_gathering_state: params.ice_gathering_state,
                    ignore_rid_pause_for_recv: params.ignore_rid_pause_for_recv,
                    write_ssrc_attributes_for_simulcast: params.write_ssrc_attributes_for_simulcast,
                };
                let (d1, should_add_id) = RTCPeerConnection::add_transceiver_sdp(
                    d,
                    &media_dtls_fingerprints,
                    media_engine,
                    transceivers,
                    ice_params,
                    candidates,
                    m,
                    params,
                )?;
                d = d1;
                should_add_id
            };

            if should_add_id {
                if bundle_match(params.match_bundle_group.as_ref(), &m.mid) {
                    append_bundle(&m.mid, &mut bundle_value, &mut bundle_count);
                } else if let Some(desc) = d.media_descriptions.last_mut() {
                    desc.media_name.port = RangedPort {
                        value: 0,
                        range: None,
                    }
                }
            }
        }

        if !params.media_description_fingerprint {
            for fingerprint in dtls_fingerprints {
                d = d.with_fingerprint(
                    fingerprint.algorithm.clone(),
                    fingerprint.value.to_uppercase(),
                );
            }
        }

        if params.is_ice_lite {
            // RFC 5245 S15.3
            d = d.with_value_attribute(ATTR_KEY_ICELITE.to_owned(), ATTR_KEY_ICELITE.to_owned());
        }

        if params.is_extmap_allow_mixed {
            // RFC 8285 6.
            d = d.with_property_attribute(ATTR_KEY_EXTMAP_ALLOW_MIXED.to_owned());
        }

        if bundle_count > 0 {
            d = d.with_value_attribute(ATTR_KEY_GROUP.to_owned(), bundle_value);
        }

        Ok(d)
    }
}

pub(crate) fn get_mid_value(media: &MediaDescription) -> Option<&String> {
    for attr in &media.attributes {
        if attr.key == "mid" {
            return attr.value.as_ref();
        }
    }
    None
}

pub(crate) fn get_peer_direction(media: &MediaDescription) -> RTCRtpTransceiverDirection {
    for a in &media.attributes {
        let direction = RTCRtpTransceiverDirection::from(a.key.as_str());
        if direction != RTCRtpTransceiverDirection::Unspecified {
            return direction;
        }
    }
    RTCRtpTransceiverDirection::Unspecified
}

pub(crate) fn extract_fingerprint(desc: &SessionDescription) -> Result<(String, String)> {
    let mut fingerprints = vec![];

    if let Some(fingerprint) = desc.attribute("fingerprint") {
        fingerprints.push(fingerprint.clone());
    }

    for m in &desc.media_descriptions {
        if let Some(fingerprint) = m.attribute("fingerprint").and_then(|o| o) {
            fingerprints.push(fingerprint.to_owned());
        }
    }

    if fingerprints.is_empty() {
        return Err(Error::ErrSessionDescriptionNoFingerprint);
    }

    for m in 1..fingerprints.len() {
        if fingerprints[m] != fingerprints[0] {
            return Err(Error::ErrSessionDescriptionConflictingFingerprints);
        }
    }

    let parts: Vec<&str> = fingerprints[0].split(' ').collect();
    if parts.len() != 2 {
        return Err(Error::ErrSessionDescriptionInvalidFingerprint);
    }

    Ok((parts[1].to_owned(), parts[0].to_owned()))
}

pub(crate) fn extract_ice_details(
    desc: &SessionDescription,
) -> Result<(String, String, Vec<Candidate>)> {
    let mut candidates = vec![];

    // Backup ufrag/pwd is the first inactive credentials found.
    // We will return the backup credentials to solve the corner case where
    // all media lines/transceivers are set to inactive.
    //
    // This should probably be handled in a better way by the caller.
    let mut backup_ufrag = None;
    let mut backup_pwd = None;

    let mut remote_ufrag = desc.attribute("ice-ufrag").map(|s| s.as_str());
    let mut remote_pwd = desc.attribute("ice-pwd").map(|s| s.as_str());

    for m in &desc.media_descriptions {
        let ufrag = m.attribute("ice-ufrag").and_then(|o| o);
        let pwd = m.attribute("ice-pwd").and_then(|o| o);

        if m.attribute(ATTR_KEY_INACTIVE).is_some() {
            if backup_ufrag.is_none() {
                backup_ufrag = ufrag;
            }
            if backup_pwd.is_none() {
                backup_pwd = pwd;
            }
            continue;
        }

        if remote_ufrag.is_none() {
            remote_ufrag = ufrag;
        }
        if remote_pwd.is_none() {
            remote_pwd = pwd;
        }

        if ufrag.is_some() && ufrag != remote_ufrag {
            return Err(Error::ErrSessionDescriptionConflictingIceUfrag);
        }
        if pwd.is_some() && pwd != remote_pwd {
            return Err(Error::ErrSessionDescriptionConflictingIcePwd);
        }

        for a in &m.attributes {
            if a.is_ice_candidate()
                && let Some(value) = &a.value
            {
                let c: Candidate = unmarshal_candidate(value)?;
                //let candidate = RTCIceCandidate::from(&c);
                candidates.push(c);
            }
        }
    }

    let remote_ufrag = remote_ufrag
        .or(backup_ufrag)
        .ok_or(Error::ErrSessionDescriptionMissingIceUfrag)?;
    let remote_pwd = remote_pwd
        .or(backup_pwd)
        .ok_or(Error::ErrSessionDescriptionMissingIcePwd)?;

    Ok((remote_ufrag.to_owned(), remote_pwd.to_owned(), candidates))
}

pub(crate) fn is_lite_set(desc: &SessionDescription) -> bool {
    for a in &desc.attributes {
        if a.key.trim() == ATTR_KEY_ICELITE {
            return true;
        }
    }
    false
}

pub(crate) fn get_application_media_section_sctp_port(desc: &SessionDescription) -> Option<u16> {
    for m in &desc.media_descriptions {
        if m.media_name.media == MEDIA_SECTION_APPLICATION {
            return if let Some(sctp_port_attr) =
                m.attributes.iter().find(|attr| attr.key == "sctp-port")
            {
                let sctp_port_value = sctp_port_attr.value.as_ref();

                sctp_port_value.and_then(|attr| attr.parse::<u16>().ok())
            } else if let Some(sctp_port_attr) =
                m.attributes.iter().find(|attr| attr.key == "sctpmap")
            {
                if let Some(sctp_port_attr_value) = sctp_port_attr.value.as_ref() {
                    sctp_port_attr_value
                        .split(" ")
                        .next()
                        .and_then(|port| u16::from_str(port).ok())
                } else {
                    None
                }
            } else {
                None
            };
        }
    }

    None
}

pub(crate) fn get_application_media_section_max_message_size(
    desc: &SessionDescription,
) -> Option<u32> {
    get_application_media(desc)?
        .attribute(ATTR_KEY_MAX_MESSAGE_SIZE)??
        .parse()
        .ok()
}

pub(crate) fn get_by_mid<'a>(
    search_mid: &str,
    desc: &'a session_description::RTCSessionDescription,
) -> Option<&'a MediaDescription> {
    if let Some(parsed) = &desc.parsed {
        for m in &parsed.media_descriptions {
            if let Some(mid) = m.attribute(ATTR_KEY_MID).flatten()
                && mid == search_mid
            {
                return Some(m);
            }
        }
    }
    None
}

pub(crate) fn get_application_media(desc: &SessionDescription) -> Option<&MediaDescription> {
    desc.media_descriptions
        .iter()
        .find(|media_description| media_description.media_name.media == MEDIA_SECTION_APPLICATION)
}

/// have_data_channel return MediaDescription with MediaName equal application
pub(crate) fn have_data_channel(
    desc: &session_description::RTCSessionDescription,
) -> Option<&MediaDescription> {
    get_application_media(desc.parsed.as_ref()?)
}

pub(crate) fn codecs_from_media_description(
    m: &MediaDescription,
) -> Result<Vec<RTCRtpCodecParameters>> {
    let s = SessionDescription {
        media_descriptions: vec![m.clone()],
        ..Default::default()
    };

    let mut out = vec![];
    for payload_str in &m.media_name.formats {
        let payload_type: PayloadType = payload_str.parse::<u8>()?;
        let codec = match s.get_codec_for_payload_type(payload_type) {
            Ok(codec) => codec,
            Err(err) => {
                if payload_type == 0 {
                    continue;
                }
                return Err(err);
            }
        };

        let channels = codec.encoding_parameters.parse::<u16>().unwrap_or(0);

        let mut feedback = vec![];
        for raw in &codec.rtcp_feedback {
            let split: Vec<&str> = raw.split(' ').collect();

            let entry = if split.len() == 2 {
                RTCPFeedback {
                    typ: split[0].to_string(),
                    parameter: split[1].to_string(),
                }
            } else {
                RTCPFeedback {
                    typ: split[0].to_string(),
                    parameter: String::new(),
                }
            };

            feedback.push(entry);
        }

        out.push(RTCRtpCodecParameters {
            rtp_codec: RTCRtpCodec {
                mime_type: m.media_name.media.clone() + "/" + codec.name.as_str(),
                clock_rate: codec.clock_rate,
                channels,
                sdp_fmtp_line: codec.fmtp.clone(),
                rtcp_feedback: feedback,
            },
            payload_type,
        })
    }

    Ok(out)
}

pub(crate) fn rtp_extensions_from_media_description(
    m: &MediaDescription,
) -> Result<HashMap<String, u16>> {
    let mut out = HashMap::new();

    for a in &m.attributes {
        if a.key == ATTR_KEY_EXT_MAP {
            let a_str = a.to_string();
            let mut reader = BufReader::new(a_str.as_bytes());
            let e = ExtMap::unmarshal(&mut reader)?;

            if let Some(uri) = e.uri {
                out.insert(uri.to_string(), e.value);
            }
        }
    }

    Ok(out)
}

/// update_sdp_origin saves sdp.Origin in PeerConnection when creating 1st local SDP;
/// for subsequent calling, it updates Origin for SessionDescription from saved one
/// and increments session version by one.
/// <https://tools.ietf.org/html/draft-ietf-rtcweb-jsep-25#section-5.2.2>
pub(crate) fn update_sdp_origin(origin: &mut Origin, d: &mut SessionDescription) {
    if origin.session_version == 0 {
        // store
        origin.session_version = d.origin.session_version;
        //atomic.StoreUint64(&origin.SessionID, d.Origin.SessionID)
        origin.session_id = d.origin.session_id;
    } else {
        // load
        /*for { // awaiting for saving session id
            d.Origin.SessionID = atomic.LoadUint64(&origin.SessionID)
            if d.Origin.SessionID != 0 {
                break
            }
        }*/
        d.origin.session_id = origin.session_id;

        //d.Origin.SessionVersion = atomic.AddUint64(&origin.SessionVersion, 1)
        origin.session_version += 1;
        d.origin.session_version += 1;
    }
}

pub(crate) fn is_ext_map_allow_mixed_set(
    remote_description: Option<&RTCSessionDescription>,
) -> bool {
    remote_description.is_some_and(|r| {
        r.parsed
            .as_ref()
            .is_some_and(|parsed| parsed.has_attribute(ATTR_KEY_EXTMAP_ALLOW_MIXED))
    })
}

/// Checks if the SDP indicates trickle ICE support.
///
/// Per RFC 8838 and RFC 9429 section 4.1.17, trickle ICE support is indicated by
/// the presence of "trickle" in space-separated "ice-options" attribute values
/// at either session level or media level.
///
/// # Arguments
///
/// * `desc` - The parsed SDP session description
///
/// # Returns
///
/// `true` if trickle ICE is supported, `false` otherwise
pub(crate) fn has_ice_trickle_option(desc: &SessionDescription) -> bool {
    // Check session-level ice-options attribute
    if let Some(value) = desc.attribute("ice-options")
        && value.split_whitespace().any(|opt| opt == "trickle")
    {
        return true;
    }

    // Check media-level ice-options attribute
    for media in &desc.media_descriptions {
        if let Some(Some(value)) = media.attribute("ice-options")
            && value.split_whitespace().any(|opt| opt == "trickle")
        {
            return true;
        }
    }

    false
}
