//! Internal transport types used by [`super::cluster::SimulatedCluster`].

use crate::{
    db::TidalDb,
    replication::{
        WalSegmentId,
        shard::{RegionId, ShardId},
        transport::{Transport, TransportError, WalSegmentPayload},
    },
};

// ── ChannelTransport: bidirectional crossbeam transport ─────────────────

/// Bidirectional crossbeam-channel transport for in-process cluster simulation.
///
/// Wraps both sender and receiver so `SimulatedCluster` can call
/// `send_segment` to ship WAL payloads and `spawn_receiver` can call
/// `recv_segment` to apply them.
pub(super) struct ChannelTransport {
    pub(super) local_shard: ShardId,
    pub(super) tx: crossbeam::channel::Sender<WalSegmentPayload>,
    pub(super) rx: crossbeam::channel::Receiver<WalSegmentPayload>,
}

impl Transport for ChannelTransport {
    fn send_segment(&self, _to: ShardId, payload: WalSegmentPayload) -> Result<(), TransportError> {
        self.tx.send(payload).map_err(|_| TransportError::Closed)
    }

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

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

/// Receive-only crossbeam transport handed to the receiver thread.
///
/// Crucially this holds **only** the receiver end. The matching `Sender` lives
/// in [`super::cluster::SimulatedCluster`]; dropping the cluster drops every
/// sender, which closes the channel and makes [`recv_segment`] return `None`,
/// letting the receiver thread exit so its `SegmentReceiverHandle` can be
/// joined. (A bidirectional [`ChannelTransport`] cannot be joined on shutdown:
/// the receiver thread's own `Arc` keeps a `Sender` alive, so `recv` would
/// block forever.)
///
/// [`recv_segment`]: Transport::recv_segment
pub(super) struct RecvOnlyChannelTransport {
    pub(super) local_shard: ShardId,
    pub(super) rx: crossbeam::channel::Receiver<WalSegmentPayload>,
}

impl Transport for RecvOnlyChannelTransport {
    fn send_segment(
        &self,
        _to: ShardId,
        _payload: WalSegmentPayload,
    ) -> Result<(), TransportError> {
        // The receiver thread never ships; only the cluster's send side does.
        Err(TransportError::Closed)
    }

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

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

// ── Batch log entry ──────────────────────────────────────────────────────

pub(super) struct BatchEntry {
    /// Which leader shard produced this batch.
    pub(super) source_shard: ShardId,
    /// 1-indexed sequence number scoped to `source_shard`.
    pub(super) seqno: u64,
    /// Encoded WAL batch bytes (from [`crate::wal::format::batch::encode_batch`]).
    pub(super) bytes: Vec<u8>,
}

// ── Payload construction ───────────────────────────────────────────────────

/// Build the single-event [`WalSegmentPayload`] for one replicated batch.
///
/// Both the eager ship path (`SimulatedCluster::write_signal`) and the recovery
/// path ([`redeliver_missed`]) emit exactly the same payload shape: a one-event
/// batch in [`RegionId::SINGLE`] whose `first_seq == last_seq == seqno`. Sharing
/// the construction here keeps the two sites from drifting on the load-bearing
/// `event_count`/`leader_last_seq` invariants (the receiver's monotonic
/// `advance` relies on `leader_last_seq == seqno`).
pub(super) const fn single_event_payload(
    source_shard: ShardId,
    seqno: u64,
    bytes: Vec<u8>,
) -> WalSegmentPayload {
    WalSegmentPayload {
        id: WalSegmentId::new(RegionId::SINGLE, source_shard, seqno),
        bytes,
        event_count: 1,
        // Single-event batch with first_seq = seqno → last WAL seq = seqno.
        leader_last_seq: seqno,
    }
}

// ── Re-delivery helper ────────────────────────────────────────────────────

/// Re-deliver all batch-log entries not yet applied to `db`, sending through
/// the given transport.
///
/// Called by `SimulatedCluster::heal_region` for immediate recovery after a
/// partition is healed, and by `await_convergence` during the polling loop.
///
/// # Load-bearing invariants
///
/// Re-delivery correctness rests on two invariants that hold today but are easy
/// to break silently:
///
/// 1. **Per-source FIFO ordering.** `log` is appended to in strictly increasing
///    `seqno` order per `source_shard` (`SimulatedCluster::write_signal` bumps
///    and commits the seqno under the `leader_seqnos` lock, then pushes the
///    `BatchEntry`), and we iterate `log` in that same order. The receiver
///    applies via `ReplicationState::advance`, which is monotonic per shard, so
///    a batch with `seqno = applied + 1` must arrive before `applied + 2` can be
///    accepted. Re-shipping out of order — or interleaving sources without
///    preserving each source's order — would leave gaps that never close. The
///    `entry.seqno > applied` gate below is what makes the scan safe to run
///    repeatedly (every poll) against the *same* growing log.
///
/// 2. **Idempotent re-application.** The same batch may be shipped more than
///    once: once eagerly by `write_signal` and again here on every
///    `await_convergence` poll and on `heal_region`. Re-applying an already-
///    applied `seqno` must be a no-op. We enforce that on the *send* side with
///    the `entry.seqno > applied` check, and the receiver enforces it again on
///    the *apply* side because `ReplicationState::advance` ignores any
///    `seqno <= applied`. Both guards are required: dropping the send-side check
///    would flood the transport with redundant payloads; relying on it alone
///    (without the receiver's monotonic advance) would double-apply on any race
///    between the `applied_seqno` read here and the receiver thread's advance.
///
/// 3. **Destination is the follower's own shard.** `send_segment`'s first
///    argument is the DESTINATION peer, and a follower's transport only knows
///    its own shard as a peer (the gRPC self-loop wiring in
///    `tidal-server::cluster`). The eager path ships with
///    `send_segment(ShardId(region.0), …)`; re-delivery must match it via
///    `transport.local_shard()` — the follower this transport belongs to.
///    Shipping to `entry.source_shard` (the LEADER's shard) fails the peer
///    lookup on every `GrpcTransport` and recovery silently never happens.
///    `ChannelTransport` ignores the destination, which is exactly why the
///    in-process suite cannot catch that regression — the unit test below
///    locks it instead. The payload itself still carries `entry.source_shard`:
///    the receiver advances `applied_seqno` PER SOURCE shard.
pub(super) fn redeliver_missed(transport: &dyn Transport, db: &TidalDb, log: &[BatchEntry]) {
    let dest = transport.local_shard();
    for entry in log {
        // Read the follower's high-water-mark per source shard. The
        // `seqno > applied` gate enforces both invariants documented above:
        // it skips already-applied batches (idempotency) and, combined with the
        // FIFO append order of `log`, ships missing batches in seqno order.
        let applied = db
            .replication_state()
            .applied_seqno(entry.source_shard)
            .unwrap_or(0);
        if entry.seqno > applied {
            let payload =
                single_event_payload(entry.source_shard, entry.seqno, entry.bytes.clone());
            // Mirror the eager path's observability contract: recovery is
            // best-effort, but a swallowed ship error leaves an operator
            // staring at a follower that never catches up. WARN, don't drop.
            if let Err(e) = transport.send_segment(dest, payload) {
                tracing::warn!(
                    dest_shard = dest.0,
                    source_shard = entry.source_shard.0,
                    seqno = entry.seqno,
                    error = %e,
                    "re-delivery ship failed; will retry on next heal/convergence pass"
                );
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    /// Locks the load-bearing payload-shape invariants of the single source of
    /// truth that both `SimulatedCluster::write_signal` (W2 fix) and
    /// [`redeliver_missed`] now share: a one-event batch in [`RegionId::SINGLE`]
    /// whose `leader_last_seq == seqno`. The receiver's monotonic `advance`
    /// relies on `leader_last_seq == seqno`; a future edit that breaks either
    /// invariant on this single helper fails here instead of silently diverging
    /// between the eager-ship and recovery sites.
    #[test]
    fn single_event_payload_holds_replication_invariants() {
        let shard = ShardId(3);
        let seqno = 42;
        let bytes = vec![1u8, 2, 3, 4];
        let payload = single_event_payload(shard, seqno, bytes.clone());

        assert_eq!(payload.event_count, 1, "must be a single-event batch");
        assert_eq!(
            payload.leader_last_seq, seqno,
            "receiver advance relies on leader_last_seq == seqno"
        );
        assert_eq!(payload.id.region_id, RegionId::SINGLE);
        assert_eq!(payload.id.shard_id, shard);
        assert_eq!(payload.id.seqno, seqno);
        assert_eq!(payload.bytes, bytes);
    }

    /// A transport that records every `(dest, payload-source)` pair. Unlike
    /// [`ChannelTransport`] (which IGNORES the destination — the reason the
    /// in-process suite cannot catch a wrong-destination regression), this
    /// mock asserts on it.
    struct RecordingTransport {
        local: ShardId,
        sent: std::sync::Mutex<Vec<(ShardId, ShardId, u64)>>, // (dest, payload source, seqno)
    }

    impl Transport for RecordingTransport {
        fn send_segment(
            &self,
            to: ShardId,
            payload: WalSegmentPayload,
        ) -> Result<(), TransportError> {
            self.sent
                .lock()
                .unwrap()
                .push((to, payload.id.shard_id, payload.id.seqno));
            Ok(())
        }
        fn recv_segment(&self) -> Option<WalSegmentPayload> {
            None
        }
        fn local_shard(&self) -> ShardId {
            self.local
        }
    }

    /// Re-delivery must ship to the FOLLOWER'S OWN shard (`local_shard`) —
    /// never to `entry.source_shard` (the leader). A follower's gRPC
    /// transport only knows itself as a peer (self-loop wiring), so the wrong
    /// destination fails the peer lookup and partition recovery silently
    /// never happens (the m8p9 heal bug). The payload, by contrast, must keep
    /// the LEADER's shard: the receiver advances `applied_seqno` per source.
    #[test]
    fn redeliver_ships_to_local_shard_with_source_payload() {
        let leader_shard = ShardId(0);
        let follower_shard = ShardId(2);
        let transport = RecordingTransport {
            local: follower_shard,
            sent: std::sync::Mutex::new(Vec::new()),
        };
        let db = crate::TidalDb::builder()
            .ephemeral()
            .open()
            .expect("open ephemeral db");
        let log = vec![
            BatchEntry {
                source_shard: leader_shard,
                seqno: 1,
                bytes: vec![1],
            },
            BatchEntry {
                source_shard: leader_shard,
                seqno: 2,
                bytes: vec![2],
            },
        ];

        redeliver_missed(&transport, &db, &log);

        let sent = transport.sent.lock().unwrap();
        assert_eq!(sent.len(), 2, "both unapplied entries must re-ship");
        for (dest, payload_source, _seqno) in sent.iter() {
            assert_eq!(
                *dest, follower_shard,
                "re-delivery destination must be the follower's own shard \
                 (transport.local_shard()), not the leader's"
            );
            assert_eq!(
                *payload_source, leader_shard,
                "payload must carry the SOURCE shard so the receiver advances \
                 applied_seqno for the right origin"
            );
        }
        assert_eq!(
            (sent[0].2, sent[1].2),
            (1, 2),
            "per-source FIFO order must be preserved"
        );
    }
}
