// Copyright 2015 The etcd Authors
// Copyright 2026 Leo Cheng
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

///|
/// How a linearizable read is confirmed (etcd's `ReadOnlyOption`).
///
/// `Safe` confirms every read with a fresh heartbeat quorum, so it holds even
/// under unbounded clock drift — etcd's default and recommended setting.
/// `LeaseBased` trusts the leader's election lease instead, saving the round
/// trip at the cost of depending on bounded clock drift; etcd requires
/// check-quorum to be on when it is selected.
pub enum ReadOnlyOption {
  Safe
  LeaseBased
} derive(Eq)

///|
/// Select how this leader confirms linearizable reads (etcd's
/// `Config.ReadOnlyOption`). `Safe` is etcd's default; this server defaults to
/// `LeaseBased` for backward compatibility with existing callers, so a
/// deployment that wants etcd's default must ask for `Safe` explicitly.
pub fn RaftNode::set_read_only_option(
  self : RaftNode,
  option : ReadOnlyOption,
) -> Unit {
  self.read_only.safe = option is Safe
}

///|
/// The read-confirmation mode this server is currently using.
pub fn RaftNode::read_only_option(self : RaftNode) -> ReadOnlyOption {
  if self.read_only.safe {
    Safe
  } else {
    LeaseBased
  }
}

///|
/// One in-flight linearizable read: the caller's opaque context, the leader's
/// commit index captured when the read was requested (Raft §6.4), and the id of
/// the server that originated the read — the leader itself for a local read, or a
/// follower that forwarded a `ReadIndex`, so the confirmed index routes back to
/// the right requester.
priv struct ReadReq {
  // The caller's opaque context, echoed back with the confirmed index so the
  // originator can match the answer to its request.
  context : Bytes
  index : UInt64
  from : String
}

///|
/// The read-index bookkeeping a leader keeps for linearizable reads (etcd's
/// `readOnly`, current design). In `ReadOnlySafe` mode confirmation is by an
/// internal *position* counter, not the caller's context: each heartbeat carries
/// the position `confirmed + len(unconfirmed)`, so a single quorum acknowledgement
/// releases every currently-unconfirmed read at once. This is what etcd switched
/// to (see its "use an internally defined context" note) and avoids the collision
/// where two reads sharing a context would clobber each other's ack state. In
/// `ReadOnlyLeaseBased` mode the leader trusts its election lease instead.
struct ReadOnly {
  mut safe : Bool
  // Each voter → the highest read-confirmation position it has acknowledged.
  acks : Map[String, UInt64]
  // Reads awaiting quorum confirmation, in request order.
  unconfirmed : Array[ReadReq]
  // How many reads have already been confirmed and drained from the front.
  mut confirmed : UInt64
  ready : Array[ReadState]
}

///|
/// A fresh, lease-based read tracker (matching etcd's default).
fn ReadOnly::new() -> ReadOnly {
  { safe: false, acks: Map([]), unconfirmed: [], confirmed: 0, ready: [] }
}

///|
/// The 8-byte little-endian read-confirmation position to stamp into a heartbeat
/// (etcd's `heartbeatCtx`), so a quorum ack confirms every currently-unconfirmed
/// read at once. Empty when there is nothing to confirm.
fn ReadOnly::heartbeat_ctx(self : ReadOnly) -> Bytes {
  if self.unconfirmed.is_empty() {
    return b""
  }
  encode_u64_le(self.confirmed + self.unconfirmed.length().to_uint64())
}

///|
/// Record a read request originated by `from` (etcd's `addRequest`): just append
/// — confirmation is positional, so no per-read ack set is kept.
fn ReadOnly::add_request(
  self : ReadOnly,
  index : UInt64,
  context : Bytes,
  from : String,
) -> Unit {
  self.unconfirmed.push({ context, index, from })
}

///|
/// Record that voter `id` acknowledged a heartbeat carrying position `ctx`
/// (etcd's `recvAck`): keep the highest position each voter has confirmed.
fn ReadOnly::recv_ack(self : ReadOnly, id : String, ctx : Bytes) -> Unit {
  if !ctx.is_empty() {
    let pos = decode_u64_le(ctx)
    let cur = self.acks.get(id).unwrap_or(0)
    if pos > cur {
      self.acks[id] = pos
    }
  }
}

///|
/// Confirm as many reads as the quorum has now acknowledged (etcd's
/// `maybeAdvance`): the quorum-committed position over the per-voter acks
/// releases that many reads from the front of `unconfirmed`, in order.
fn ReadOnly::maybe_advance(
  self : ReadOnly,
  config : Membership,
) -> Array[ReadReq] {
  let new_confirmed = config.committed_index(self.acks)
  if new_confirmed <= self.confirmed {
    return []
  }
  let n = (new_confirmed - self.confirmed).to_int()
  let released : Array[ReadReq] = []
  let rest : Array[ReadReq] = []
  for i, req in self.unconfirmed {
    if i < n {
      released.push(req)
    } else {
      rest.push(req)
    }
  }
  self.unconfirmed.clear()
  for req in rest {
    self.unconfirmed.push(req)
  }
  self.confirmed = new_confirmed
  released
}

///|
/// Encode a 64-bit value as 8 little-endian bytes (etcd uses
/// `binary.LittleEndian` for the read-confirmation position).
fn encode_u64_le(v : UInt64) -> Bytes {
  let arr : Array[Byte] = []
  for i in 0..<8 {
    arr.push(((v >> (i * 8)) & 0xff).to_byte())
  }
  Bytes::from_array(arr)
}

///|
/// Decode 8 little-endian bytes back to a 64-bit value.
fn decode_u64_le(b : Bytes) -> UInt64 {
  let mut v = 0UL
  let n = if b.length() < 8 { b.length() } else { 8 }
  for i in 0.. Array[ReadState] {
  let out = self.ready.copy()
  self.ready.clear()
  out
}

///|
/// Switch this server to the linearizable `ReadOnlySafe` read mode: a read is
/// confirmed by a fresh heartbeat quorum rather than the election lease.
pub fn RaftNode::enable_read_only_safe(self : RaftNode) -> Unit {
  self.read_only.safe = true
}

///|
/// Request a linearizable read (Raft §6.4). Returns the heartbeats to broadcast
/// (in `ReadOnlySafe` mode) so a quorum can confirm the leader is current; the
/// confirmed read index is later collected with `take_read_states`. A follower,
/// or a leader that has not yet committed an entry in its own term, serves
/// nothing. In `ReadOnlyLeaseBased` mode (the default) a read is confirmed at
/// once when the lease is valid.
pub fn RaftNode::request_read_index(
  self : RaftNode,
  context : Bytes,
) -> Array[Message] {
  if self.core.role() != Leader {
    return []
  }
  self.lead_read_index(self.id, context)
}

///|
/// Serve a read request on the leader (Raft §6.4), for a read originated by
/// `from` — the leader itself for a local read, or a follower that forwarded a
/// `ReadIndex`. In `ReadOnlySafe` mode the read is held until a heartbeat quorum
/// confirms the leadership, so the returned messages are the heartbeats to
/// broadcast; in lease mode a valid lease confirms it at once. A confirmed read
/// is delivered to its originator (recorded locally, or answered with a
/// `ReadIndexResp`).
fn RaftNode::lead_read_index(
  self : RaftNode,
  from : String,
  context : Bytes,
) -> Array[Message] {
  // A singleton leader (its only voter) has its leadership trivially confirmed,
  // so the read is answered at once against the commit index — etcd's
  // `IsSingleton` fast path, taken before the in-term-commit gate below.
  // `IsSingleton` counts *voters* only (`len(Voters[0]) == 1 && len(Voters[1]) ==
  // 0`), so a lone voter with learners still qualifies: learners never confirm a
  // read, and waiting on one would let a down learner stall a read the sole voter
  // could serve itself.
  if self.config.size() == 1 && !self.config.is_joint() {
    return self.deliver_read({ from, index: self.core.commit_index, context })
  }
  // The read must be anchored at an index from the current term, else the commit
  // index may not yet reflect the latest leader's writes (§5.4.2). Until the
  // leader has committed in its term the request is *queued*, not dropped: etcd
  // holds it in `pendingReadIndexMessages` and releases it on the first in-term
  // commit, so a read issued right after an election is answered rather than
  // lost.
  if self.core.term_at(self.core.commit_index) != self.core.current_term() {
    self.pending_read_index.push((from, context))
    return []
  }
  if self.read_only.safe {
    self.read_only.add_request(self.core.commit_index, context, from)
    // The leader implicitly acknowledges the new position itself (etcd's
    // recvAck(r.id, heartbeatCtx)), then broadcasts a heartbeat stamped with it.
    self.read_only.recv_ack(self.id, self.read_only.heartbeat_ctx())
    self.bcast_heartbeat()
  } else if self.quorum_active() {
    self.deliver_read({ from, index: self.core.commit_index, context })
  } else {
    []
  }
}

///|
/// Route a confirmed read to its originator: a local read is recorded for
/// `take_read_states`; a read forwarded by a follower is answered with a
/// `ReadIndexResp` carrying the confirmed index and the caller's context.
fn RaftNode::deliver_read(self : RaftNode, req : ReadReq) -> Array[Message] {
  if req.from == self.id {
    self.read_only.ready.push({ index: req.index, request_ctx: req.context })
    []
  } else {
    [
      Message::new(
        self.id,
        req.from,
        ReadIndexResp({
          term: self.core.current_term(),
          index: req.index,
          context: req.context,
        }),
      ),
    ]
  }
}

///|
/// Answer the read-index requests that were queued before the leader had
/// committed an entry in its current term, now that it has (etcd's
/// `releasePendingReadIndexMessages`). Returns any messages the served reads
/// produce (heartbeats to confirm leadership under `ReadOnlySafe`). A no-op
/// while the queue is empty or the leader still has no in-term commit.
fn RaftNode::release_pending_read_index(self : RaftNode) -> Array[Message] {
  if self.pending_read_index.is_empty() {
    return []
  }
  // The sole caller runs this immediately after `maybe_commit` reported a fresh
  // commit, which by §5.4.2 only advances the commit index onto a current-term
  // entry, so the in-term-commit precondition (etcd's `committedEntryInCurrentTerm`
  // guard in `releasePendingReadIndexMessages`) always already holds here.
  let pending = self.pending_read_index.copy()
  self.pending_read_index.clear()
  let out : Array[Message] = []
  for req in pending {
    for m in self.lead_read_index(req.0, req.1) {
      out.push(m)
    }
  }
  out
}

///|
/// Collect the linearizable reads confirmed since the last call. Each carries
/// the commit index the state machine must have applied before the read is
/// answered, so it observes every previously-acknowledged write.
pub fn RaftNode::take_read_states(self : RaftNode) -> Array[ReadState] {
  self.read_only.take_ready()
}