//! SNAP (SCTP Negotiation Acceleration Protocol) types.
//!
//! See [draft-hancke-tsvwg-snap](https://datatracker.ietf.org/doc/draft-hancke-tsvwg-snap/).

use std::sync::Arc;

use base64ct::{Base64, Encoding};
use sctp_proto::{ClientConfig, TransportConfig, generate_snap_token};

use super::SctpError as Error;

/// Build the WebRTC transport config with unlimited retransmits.
///
/// For WebRTC, we never want to give up retransmitting init and data packets.
/// The connectivity is in ICE, and SCTP should not give up until ICE gives up.
pub(super) fn webrtc_transport_config() -> Arc<TransportConfig> {
    Arc::new(
        TransportConfig::default()
            .with_max_init_retransmits(None)
            .with_max_data_retransmits(None),
    )
}

/// Out-of-band SCTP INIT data for SNAP negotiation.
///
/// Holds the local and remote SCTP INIT chunks exchanged via signaling
/// (e.g. SDP `a=sctp-init`). When both are present, the SCTP association
/// skips the 4-way handshake and goes directly to established state.
///
/// Typical flow:
///
/// 1. Create `SctpInitData`.
/// 2. Call [`Self::local_init_chunk()`] and signal the returned bytes.
/// 3. Receive the peer's INIT bytes over signaling.
/// 4. Call [`Self::set_remote_init_chunk()`].
/// 5. Pass the populated value to
///    [`DirectApi::start_sctp_with_snap()`][crate::change::DirectApi::start_sctp_with_snap].
///
/// The local INIT chunk is lazily generated and cached on first access.
///
/// # Example
/// ```
/// use str0m::channel::SctpInitData;
///
/// let mut data = SctpInitData::new();
///
/// // Get local INIT chunk to send to remote peer
/// let local_init = data.local_init_chunk().expect("valid chunk");
///
/// // Later, set the remote INIT chunk received from peer
/// // data.set_remote_init_chunk(remote_init_bytes);
/// ```
#[derive(Debug, Clone)]
pub struct SctpInitData {
    pub(crate) transport: Arc<TransportConfig>,
    pub(crate) local_init: Option<Vec<u8>>,
    pub(crate) remote_init: Option<Vec<u8>>,
}

impl Default for SctpInitData {
    fn default() -> Self {
        SctpInitData {
            transport: webrtc_transport_config(),
            local_init: None,
            remote_init: None,
        }
    }
}

impl SctpInitData {
    /// Creates new default SCTP INIT data.
    ///
    /// By default, max init and data retransmits are set to `None` (unlimited),
    /// which is recommended for WebRTC where connectivity is managed by ICE.
    pub fn new() -> Self {
        Self::default()
    }

    /// Get the local INIT chunk bytes for out-of-band signaling.
    ///
    /// This generates a fresh INIT chunk using the transport config parameters.
    /// The result can be exchanged with the remote peer via a signaling channel,
    /// allowing both sides to skip the SCTP 4-way handshake.
    ///
    /// The generated bytes are cached so subsequent calls return the same INIT.
    ///
    /// If you plan to call `start_sctp_with_snap()`, call this method first so
    /// the local INIT is captured in the `SctpInitData` you pass in.
    pub fn local_init_chunk(&mut self) -> Result<Vec<u8>, Error> {
        if let Some(init) = &self.local_init {
            return Ok(init.clone());
        }
        let init = generate_snap_token(&self.transport)?.to_vec();
        self.local_init = Some(init.clone());
        Ok(init)
    }

    /// Get the local INIT chunk as a base64-encoded string for out-of-band signaling.
    ///
    /// This generates a fresh INIT chunk using the transport config parameters.
    /// The result can be exchanged with the remote peer via a signaling channel,
    /// allowing both sides to skip the SCTP 4-way handshake.
    ///
    /// The generated bytes are cached so subsequent calls return the same string.
    ///
    /// If you plan to call `start_sctp_with_snap()`, call this method first so
    /// the local INIT is captured in the `SctpInitData` you pass in.
    pub fn local_init_string(&mut self) -> Result<String, Error> {
        self.local_init_chunk().map(|c| b64_encode(&c))
    }

    /// Check if remote INIT chunk has been configured for out-of-band signaling.
    pub fn has_remote_init_chunk(&self) -> bool {
        self.remote_init.is_some()
    }

    /// Get the remote INIT chunk as a base64-encoded string, if set.
    pub fn remote_init_string(&self) -> Option<String> {
        self.remote_init.as_ref().map(|b| b64_encode(b))
    }

    /// Set the remote INIT chunk for out-of-band signaling.
    ///
    /// When both local and remote INIT chunks are exchanged via a signaling
    /// channel, the SCTP association can skip the 4-way handshake and go
    /// directly to established state.
    ///
    /// This must be called before starting SCTP with SNAP.
    pub fn set_remote_init_chunk(&mut self, value: Vec<u8>) {
        self.remote_init = Some(value);
    }

    /// Set the remote INIT chunk from a base64-encoded string.
    ///
    /// Convenience method for signaling channels that exchange strings (e.g. SDP).
    pub fn set_remote_init_string(&mut self, value: &str) -> Result<(), Error> {
        let mut buf = vec![0u8; value.len()];
        let len = Base64::decode(value, &mut buf)
            .map_err(|_| Error::InvalidSnap)?
            .len();
        buf.truncate(len);
        self.set_remote_init_chunk(buf);
        Ok(())
    }

    /// Build a `ClientConfig` from this data, consuming it.
    ///
    /// Only used internally by [`super::RtcSctp::init()`] to feed sctp-proto.
    pub(crate) fn into_client_config(self) -> ClientConfig {
        let mut config = ClientConfig::new();
        config.transport = self.transport;
        match (self.local_init, self.remote_init) {
            (Some(local), Some(remote)) => {
                config = config.with_snap(local.into(), remote.into());
            }
            (Some(_), None) | (None, Some(_)) => {
                unreachable!("SNAP requires both local and remote INIT chunks");
            }
            (None, None) => {}
        }
        config
    }
}

pub(super) fn b64_encode(data: &[u8]) -> String {
    let mut buf = vec![0u8; Base64::encoded_len(data)];
    let encoded = Base64::encode(data, &mut buf).expect("buffer sized correctly");
    encoded.to_string()
}
