//! Traffic analysis resistance — decoy traffic generation and timing jitter.
//!
//! When enabled (via the `traffic-resistance` feature), the server:
//!
//! 1. Pads all enqueued payloads to a uniform boundary using [`quicproquo_core::padding::pad_uniform`].
//! 2. Injects random jitter delays before enqueue responses to mask timing patterns.
//! 3. Runs a background decoy traffic generator that enqueues fake encrypted messages
//!    at a configurable rate to connected recipients.
//!
//! Decoy messages are indistinguishable from real padded messages on the wire.
//! Recipients detect and discard them by unpadding to an empty payload.

use std::sync::Arc;

use rand::Rng;
use tokio::sync::Notify;

use super::delivery::DeliveryService;
use super::types::EnqueueReq;

/// Configuration for traffic analysis resistance.
#[derive(Clone, Debug)]
pub struct TrafficResistanceConfig {
    /// Padding boundary in bytes (default 256). All enqueued payloads are
    /// padded to the nearest multiple of this value.
    pub padding_boundary: usize,

    /// Mean interval in milliseconds between decoy messages per recipient.
    /// Set to 0 to disable decoy traffic.
    pub decoy_interval_ms: u64,

    /// Maximum random jitter in milliseconds added before enqueue responses.
    /// Set to 0 to disable jitter.
    pub jitter_max_ms: u64,
}

impl Default for TrafficResistanceConfig {
    fn default() -> Self {
        Self {
            padding_boundary: quicproquo_core::padding::DEFAULT_PADDING_BOUNDARY,
            decoy_interval_ms: 5000,
            jitter_max_ms: 50,
        }
    }
}

/// Pad a payload to the configured uniform boundary.
pub fn pad_payload(payload: &[u8], config: &TrafficResistanceConfig) -> Vec<u8> {
    quicproquo_core::padding::pad_uniform(payload, config.padding_boundary)
}

/// Apply random jitter delay to mask timing patterns.
///
/// Sleeps for a random duration in `[0, config.jitter_max_ms)` milliseconds.
/// Does nothing if `jitter_max_ms` is 0.
pub async fn apply_jitter(config: &TrafficResistanceConfig) {
    if config.jitter_max_ms == 0 {
        return;
    }
    let jitter_ms = rand::thread_rng().gen_range(0..config.jitter_max_ms);
    if jitter_ms > 0 {
        tokio::time::sleep(std::time::Duration::from_millis(jitter_ms)).await;
    }
}

/// Spawn a background task that generates decoy traffic.
///
/// Sends decoy messages to the provided `recipient_keys` at random intervals
/// around `config.decoy_interval_ms`. The task runs until `shutdown` is notified.
///
/// Returns a `JoinHandle` for the spawned task.
pub fn spawn_decoy_generator(
    delivery: Arc<DeliveryService>,
    recipient_keys: Vec<Vec<u8>>,
    channel_id: Vec<u8>,
    config: TrafficResistanceConfig,
    shutdown: Arc<Notify>,
) -> tokio::task::JoinHandle<()> {
    tokio::spawn(async move {
        if config.decoy_interval_ms == 0 || recipient_keys.is_empty() {
            // Decoy traffic disabled or no recipients — wait for shutdown.
            shutdown.notified().await;
            return;
        }

        let base_interval = std::time::Duration::from_millis(config.decoy_interval_ms);

        loop {
            // Randomize interval: 50%–150% of base to avoid periodic patterns.
            let jitter_factor: f64 = rand::thread_rng().gen_range(0.5..1.5);
            let interval = base_interval.mul_f64(jitter_factor);

            tokio::select! {
                () = tokio::time::sleep(interval) => {}
                () = shutdown.notified() => {
                    tracing::debug!("decoy traffic generator shutting down");
                    return;
                }
            }

            // Pick a random recipient.
            let idx = rand::thread_rng().gen_range(0..recipient_keys.len());
            let recipient_key = &recipient_keys[idx];

            // Generate a decoy payload that is indistinguishable from a real padded message.
            let decoy = quicproquo_core::padding::generate_decoy(config.padding_boundary);

            let req = EnqueueReq {
                recipient_key: recipient_key.clone(),
                payload: decoy,
                channel_id: channel_id.clone(),
                ttl_secs: 60, // Short TTL for decoys.
            };

            match delivery.enqueue(req) {
                Ok(_) => {
                    tracing::trace!("decoy message injected");
                }
                Err(e) => {
                    tracing::warn!(error = %e, "failed to inject decoy message");
                }
            }
        }
    })
}

#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
    use super::*;
    use crate::storage::FileBackedStore;
    use dashmap::DashMap;

    fn test_delivery() -> (tempfile::TempDir, Arc<DeliveryService>) {
        let dir = tempfile::tempdir().unwrap();
        let store = Arc::new(FileBackedStore::open(dir.path()).unwrap());
        let svc = Arc::new(DeliveryService {
            store,
            waiters: Arc::new(DashMap::new()),
        });
        (dir, svc)
    }

    #[test]
    fn pad_payload_is_boundary_aligned() {
        let config = TrafficResistanceConfig {
            padding_boundary: 256,
            ..Default::default()
        };
        let payload = b"test message";
        let padded = pad_payload(payload, &config);
        assert_eq!(padded.len() % 256, 0);

        // Unpad should recover original.
        let unpadded = quicproquo_core::padding::unpad_uniform(&padded).unwrap();
        assert_eq!(unpadded, payload);
    }

    #[test]
    fn pad_payload_custom_boundary() {
        let config = TrafficResistanceConfig {
            padding_boundary: 512,
            ..Default::default()
        };
        let payload = vec![0xAA; 300];
        let padded = pad_payload(&payload, &config);
        assert_eq!(padded.len() % 512, 0);
        assert_eq!(padded.len(), 512);
    }

    #[tokio::test]
    async fn jitter_zero_is_noop() {
        let config = TrafficResistanceConfig {
            jitter_max_ms: 0,
            ..Default::default()
        };
        let start = std::time::Instant::now();
        apply_jitter(&config).await;
        // Should return almost immediately.
        assert!(start.elapsed() < std::time::Duration::from_millis(5));
    }

    #[tokio::test]
    async fn decoy_generator_produces_messages() {
        let (_dir, delivery) = test_delivery();
        let recipient = vec![0xFFu8; 32];
        let channel = vec![0u8; 16];
        let shutdown = Arc::new(Notify::new());

        let config = TrafficResistanceConfig {
            padding_boundary: 256,
            decoy_interval_ms: 50, // Fast interval for testing.
            jitter_max_ms: 0,
        };

        let handle = spawn_decoy_generator(
            Arc::clone(&delivery),
            vec![recipient.clone()],
            channel.clone(),
            config,
            Arc::clone(&shutdown),
        );

        // Wait enough time for at least one decoy.
        tokio::time::sleep(std::time::Duration::from_millis(200)).await;
        shutdown.notify_one();
        handle.await.unwrap();

        // Check that decoy messages were enqueued.
        let fetched = delivery
            .fetch(super::super::types::FetchReq {
                recipient_key: recipient,
                channel_id: channel,
                limit: 100,
            })
            .unwrap();

        assert!(!fetched.payloads.is_empty(), "decoy generator should have enqueued at least one message");

        // Every decoy should unpad to an empty payload.
        for env in &fetched.payloads {
            let unpadded = quicproquo_core::padding::unpad_uniform(&env.data).unwrap();
            assert!(unpadded.is_empty(), "decoy payload should unpad to empty");
        }
    }

    #[tokio::test]
    async fn decoy_generator_disabled_when_zero_interval() {
        let (_dir, delivery) = test_delivery();
        let shutdown = Arc::new(Notify::new());

        let config = TrafficResistanceConfig {
            decoy_interval_ms: 0,
            ..Default::default()
        };

        let handle = spawn_decoy_generator(
            delivery,
            vec![vec![1u8; 32]],
            vec![0u8; 16],
            config,
            Arc::clone(&shutdown),
        );

        // Signal shutdown immediately — should return without having sent anything.
        shutdown.notify_one();
        handle.await.unwrap();
    }
}
