//! In-process transport for WAL segment shipping via crossbeam channels.
//!
//! Used for integration tests and single-process multi-shard deployments.
//! Each shard gets a unique receiver and a clone of all senders, enabling
//! any-to-any communication within the process.

use std::collections::HashMap;

use crossbeam::channel::{Receiver, Sender, bounded};

use crate::replication::shard::ShardId;
use crate::replication::transport::{Transport, TransportError, WalSegmentPayload};

/// Maximum payload size in bytes (64 MB). Segments larger than this are
/// rejected to prevent unbounded memory growth in the channel buffer.
const MAX_PAYLOAD_BYTES: usize = 64 * 1024 * 1024;

/// Factory for building a set of [`InProcessTransport`] endpoints, one per shard.
///
/// Each transport owns all senders (one per shard) and its own unique receiver.
/// The channel capacity is 64 segments per shard.
pub struct InProcessTransportFactory {
    shards: Vec<ShardId>,
}

impl InProcessTransportFactory {
    /// Create a factory for the given shard IDs.
    #[must_use]
    pub fn new(shards: &[ShardId]) -> Self {
        Self {
            shards: shards.to_vec(),
        }
    }

    /// Build one `InProcessTransport` per shard.
    ///
    /// Each transport can send to any shard and receives on its own channel.
    ///
    /// # Panics
    ///
    /// Panics if a receiver is missing for a shard -- this is unreachable
    /// because the receiver map is populated from the same shard list.
    #[must_use]
    pub fn build(self) -> HashMap<ShardId, InProcessTransport> {
        let mut senders: HashMap<ShardId, Sender<WalSegmentPayload>> = HashMap::new();
        let mut receivers: HashMap<ShardId, Receiver<WalSegmentPayload>> = HashMap::new();

        for &shard_id in &self.shards {
            let (tx, rx) = bounded(64);
            senders.insert(shard_id, tx);
            receivers.insert(shard_id, rx);
        }

        let mut result = HashMap::new();
        for &local_shard in &self.shards {
            let receiver = receivers
                .remove(&local_shard)
                .expect("receiver missing for shard that was just created");
            let transport = InProcessTransport {
                local_shard,
                senders: senders.clone(),
                receiver,
            };
            result.insert(local_shard, transport);
        }
        result
    }
}

/// An in-process transport endpoint backed by crossbeam bounded channels.
///
/// Thread-safe: `Sender` and `Receiver` from crossbeam are `Send + Sync`.
pub struct InProcessTransport {
    local_shard: ShardId,
    senders: HashMap<ShardId, Sender<WalSegmentPayload>>,
    receiver: Receiver<WalSegmentPayload>,
}

impl Transport for InProcessTransport {
    fn send_segment(&self, to: ShardId, payload: WalSegmentPayload) -> Result<(), TransportError> {
        if payload.bytes.len() > MAX_PAYLOAD_BYTES {
            return Err(TransportError::PayloadTooLarge {
                size: payload.bytes.len(),
                max: MAX_PAYLOAD_BYTES,
            });
        }
        let sender = self
            .senders
            .get(&to)
            .ok_or(TransportError::UnknownPeer(to))?;
        sender.send(payload).map_err(|_| TransportError::Closed)
    }

    fn recv_segment(&self) -> Option<WalSegmentPayload> {
        self.receiver.recv().ok()
    }

    fn local_shard(&self) -> ShardId {
        self.local_shard
    }
}

#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
    use super::*;
    use crate::replication::segment_id::WalSegmentId;
    use crate::replication::shard::RegionId;

    fn make_payload(shard: ShardId, seqno: u64) -> WalSegmentPayload {
        WalSegmentPayload {
            id: WalSegmentId::new(RegionId::SINGLE, shard, seqno),
            bytes: vec![0xAB; 100],
            event_count: 5,
        }
    }

    #[test]
    fn factory_creates_one_transport_per_shard() {
        let shards = [ShardId(0), ShardId(1), ShardId(2)];
        let factory = InProcessTransportFactory::new(&shards);
        let transports = factory.build();
        assert_eq!(transports.len(), 3);
        assert!(transports.contains_key(&ShardId(0)));
        assert!(transports.contains_key(&ShardId(1)));
        assert!(transports.contains_key(&ShardId(2)));
    }

    #[test]
    fn send_and_receive_between_shards() {
        let shards = [ShardId(0), ShardId(1)];
        let factory = InProcessTransportFactory::new(&shards);
        let transports = factory.build();

        let t0 = &transports[&ShardId(0)];
        let t1 = &transports[&ShardId(1)];

        // Shard 0 sends to Shard 1.
        let payload = make_payload(ShardId(0), 42);
        t0.send_segment(ShardId(1), payload).unwrap();

        // Shard 1 receives it.
        let received = t1.recv_segment().unwrap();
        assert_eq!(received.id.seqno, 42);
        assert_eq!(received.event_count, 5);
        assert_eq!(received.bytes.len(), 100);
    }

    #[test]
    fn send_to_unknown_peer_fails() {
        let shards = [ShardId(0)];
        let factory = InProcessTransportFactory::new(&shards);
        let transports = factory.build();

        let t0 = &transports[&ShardId(0)];
        let result = t0.send_segment(ShardId(99), make_payload(ShardId(0), 1));
        assert!(result.is_err());
        let err = result.unwrap_err();
        assert!(matches!(err, TransportError::UnknownPeer(ShardId(99))));
    }

    #[test]
    fn payload_too_large_rejected() {
        let shards = [ShardId(0), ShardId(1)];
        let factory = InProcessTransportFactory::new(&shards);
        let transports = factory.build();

        let t0 = &transports[&ShardId(0)];
        let payload = WalSegmentPayload {
            id: WalSegmentId::single_node(1),
            bytes: vec![0u8; MAX_PAYLOAD_BYTES + 1],
            event_count: 0,
        };
        let result = t0.send_segment(ShardId(1), payload);
        assert!(result.is_err());
        assert!(matches!(
            result.unwrap_err(),
            TransportError::PayloadTooLarge { .. }
        ));
    }

    #[test]
    fn local_shard_returns_correct_id() {
        let shards = [ShardId(7), ShardId(8)];
        let factory = InProcessTransportFactory::new(&shards);
        let transports = factory.build();

        assert_eq!(transports[&ShardId(7)].local_shard(), ShardId(7));
        assert_eq!(transports[&ShardId(8)].local_shard(), ShardId(8));
    }

    #[test]
    fn recv_returns_none_when_all_transports_dropped() {
        let shards = [ShardId(0), ShardId(1)];
        let factory = InProcessTransportFactory::new(&shards);
        let transports = factory.build();

        // Extract shard 1's receiver before dropping everything.
        // We need to drop ALL transports (including t1 itself) because
        // each transport holds clones of all senders -- including senders
        // to its own channel.
        let t1_receiver = {
            let t1 = &transports[&ShardId(1)];
            t1.receiver.clone()
        };

        // Drop all transports -- this drops all senders.
        drop(transports);

        // With all senders dropped, recv should return None.
        assert!(t1_receiver.recv().is_err());
    }

    #[test]
    fn multiple_segments_fifo_order() {
        let shards = [ShardId(0), ShardId(1)];
        let factory = InProcessTransportFactory::new(&shards);
        let transports = factory.build();

        let t0 = &transports[&ShardId(0)];
        let t1 = &transports[&ShardId(1)];

        for seq in 1..=5u64 {
            t0.send_segment(ShardId(1), make_payload(ShardId(0), seq))
                .unwrap();
        }

        for expected_seq in 1..=5u64 {
            let received = t1.recv_segment().unwrap();
            assert_eq!(received.id.seqno, expected_seq);
        }
    }
}
