// 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.

///|
/// The kind of a single-server configuration change (Raft §6, §4.2.1).
pub(all) enum ConfChangeType {
  AddNode
  RemoveNode
  AddLearnerNode
} derive(Eq)

///|
/// A configuration change carried by a `ConfChange` log entry: which server is
/// joining or leaving. It is serialized into the entry's command so every
/// server applies the same change at the same log position.
pub(all) struct ConfChange {
  change_type : ConfChangeType
  node_id : String
} derive(Eq)

///|
/// A change that adds `id` to the cluster.
pub fn ConfChange::add(id : String) -> ConfChange {
  { change_type: AddNode, node_id: id }
}

///|
/// A change that removes `id` from the cluster.
pub fn ConfChange::remove(id : String) -> ConfChange {
  { change_type: RemoveNode, node_id: id }
}

///|
/// A change that adds `id` as a learner (a non-voting member).
pub fn ConfChange::add_learner(id : String) -> ConfChange {
  { change_type: AddLearnerNode, node_id: id }
}

///|
/// Serialize this change into a log-entry command. The encoding is a one-
/// character tag ('+' add voter, '-' remove, 'L' add learner) followed by the
/// server id, which keeps it human-readable in dumps and trivially reversible.
pub fn ConfChange::encode(self : ConfChange) -> Bytes {
  let tag = match self.change_type {
    AddNode => "+"
    RemoveNode => "-"
    AddLearnerNode => "L"
  }
  // Store the change as the UTF-16LE code units of `tag ++ id`, which is how
  // MoonBit strings read back through `to_unchecked_string`.
  let arr : Array[Byte] = []
  for c in tag + self.node_id {
    let code = c.to_int()
    arr.push((code & 0xff).to_byte())
    arr.push(((code >> 8) & 0xff).to_byte())
  }
  Bytes::from_array(arr)
}

///|
/// Recover a configuration change from a log-entry command, or `None` if the
/// bytes are not a well-formed change.
pub fn ConfChange::decode(command : Bytes) -> ConfChange? {
  let s = command.to_unchecked_string()
  if s.length() < 1 {
    return None
  }
  let id = s[1:].to_owned()
  if s.has_prefix("+") {
    Some({ change_type: AddNode, node_id: id })
  } else if s.has_prefix("-") {
    Some({ change_type: RemoveNode, node_id: id })
  } else if s.has_prefix("L") {
    Some({ change_type: AddLearnerNode, node_id: id })
  } else {
    None
  }
}

///|
/// Apply this change to a configuration in place.
pub fn ConfChange::apply_to(self : ConfChange, config : Membership) -> Unit {
  match self.change_type {
    AddNode => config.add(self.node_id)
    RemoveNode => config.remove(self.node_id)
    AddLearnerNode => config.add_learner(self.node_id)
  }
}

///|
/// The one-character tag for a single change ('+'/'-'/'L').
fn ConfChange::tag(self : ConfChange) -> String {
  match self.change_type {
    AddNode => "+"
    RemoveNode => "-"
    AddLearnerNode => "L"
  }
}

///|
/// How a `ConfChangeV2` transitions the configuration (etcd's
/// `ConfChangeTransition`): `Auto` applies a batch simply when it safely can
/// (at most one voter changed) and otherwise enters an auto-leaving joint;
/// `JointImplicit` always enters joint and auto-leaves; `JointExplicit` always
/// enters joint and waits for an explicit leave.
pub(all) enum ConfChangeTransition {
  Auto
  JointImplicit
  JointExplicit
} derive(Eq)

///|
/// A batch configuration change (etcd's `ConfChangeV2`): several single changes
/// applied atomically. An empty batch leaves joint consensus. Whether a non-empty
/// batch is applied simply or via a joint transition — and whether that joint
/// auto-leaves — is governed by `transition` (Raft §4.3, joint consensus).
pub(all) struct ConfChangeV2 {
  changes : Array[ConfChange]
  transition : ConfChangeTransition
} derive(Eq)

///|
/// Enter joint consensus with `changes`. `auto_leave` selects the implicit
/// (auto-leaving) or explicit joint transition — the historical API.
pub fn ConfChangeV2::enter_joint(
  changes : Array[ConfChange],
  auto_leave? : Bool = true,
) -> ConfChangeV2 {
  {
    changes,
    transition: if auto_leave {
      JointImplicit
    } else {
      JointExplicit
    },
  }
}

///|
/// A batch with the `Auto` transition: applied simply when it can be (at most one
/// voter changed), otherwise as an auto-leaving joint change — etcd's default.
pub fn ConfChangeV2::auto(changes : Array[ConfChange]) -> ConfChangeV2 {
  { changes, transition: Auto }
}

///|
/// Leave joint consensus (an empty batch).
pub fn ConfChangeV2::leave_joint() -> ConfChangeV2 {
  { changes: [], transition: Auto }
}

///|
/// Whether this change leaves a joint configuration (etcd's
/// `ConfChangeV2.LeaveJoint`). This is the case only for the `Auto` transition
/// with no changes: an *explicit* joint transition carrying no changes still
/// *enters* an (empty) joint config and must not be mistaken for a leave, which
/// is exactly the complement of `enters_joint`.
pub fn ConfChangeV2::is_leave(self : ConfChangeV2) -> Bool {
  self.transition == Auto && self.changes.is_empty()
}

///|
/// Whether this batch enters a joint configuration, and if so whether that joint
/// auto-leaves (etcd's `ConfChangeV2.EnterJoint`). `Auto` with at most one change
/// is applied *simply* — no joint; anything else is joint, auto-leaving unless
/// the transition is explicit.
pub fn ConfChangeV2::enters_joint(self : ConfChangeV2) -> (Bool, Bool) {
  if self.transition != Auto || self.changes.length() > 1 {
    let auto = match self.transition {
      Auto | JointImplicit => true
      JointExplicit => false
    }
    (auto, true)
  } else {
    (false, false)
  }
}

///|
/// Whether this joint change auto-leaves once committed.
pub fn ConfChangeV2::auto_leave(self : ConfChangeV2) -> Bool {
  self.enters_joint().0
}

///|
fn ConfChangeTransition::tag(self : ConfChangeTransition) -> String {
  match self {
    Auto => "a"
    JointImplicit => "i"
    JointExplicit => "e"
  }
}

///|
/// Serialize as `V` + transition tag (`a`/`i`/`e`) + `;`-separated ``
/// changes. The `V` prefix distinguishes a batch from a single change on decode.
pub fn ConfChangeV2::encode(self : ConfChangeV2) -> Bytes {
  let mut s = "V" + self.transition.tag()
  for c in self.changes {
    s = s + ";" + c.tag() + c.node_id
  }
  let arr : Array[Byte] = []
  for ch in s {
    let code = ch.to_int()
    arr.push((code & 0xff).to_byte())
    arr.push(((code >> 8) & 0xff).to_byte())
  }
  Bytes::from_array(arr)
}

///|
/// Recover a batch change, or `None` if the bytes are not a well-formed batch.
pub fn ConfChangeV2::decode(command : Bytes) -> ConfChangeV2? {
  let s = command.to_unchecked_string()
  if !s.has_prefix("V") {
    return None
  }
  let transition = if s.length() >= 2 {
    match s[1:2].to_owned() {
      "i" => JointImplicit
      "e" => JointExplicit
      _ => Auto
    }
  } else {
    Auto
  }
  let changes : Array[ConfChange] = []
  // Split the remainder on ';', skipping the leading flag segment.
  let mut cur = ""
  let mut seg = 0
  fn flush() -> Unit {
    if seg > 0 && cur.length() >= 1 {
      let id = cur[1:].to_owned()
      let ct = match cur[0:1].to_owned() {
        "+" => Some(AddNode)
        "-" => Some(RemoveNode)
        "L" => Some(AddLearnerNode)
        _ => None
      }
      if ct is Some(t) {
        changes.push({ change_type: t, node_id: id })
      }
    }
    cur = ""
  }

  for ch in s {
    if ch == ';' {
      flush()
      seg = seg + 1
    } else {
      cur = cur + ch.to_string()
    }
  }
  flush()
  Some({ changes, transition })
}