///|
/// Errors raised while constructing or updating a node model.
pub(all) suberror NodeError {
  EmptyNodeId
  InvalidTimestamp
  InvalidTransition(String)
  InvalidHealthUpdate
} derive(Debug, Eq)

///|
/// Lifecycle states for an EdgeWeaver node.
pub(all) enum NodeLifecycle {
  New
  Enrolled
  Online
  Offline
  Retired
} derive(Debug, Eq, ToJson, FromJson)

///|
/// Health states reported by a node or an operator.
pub(all) enum NodeHealth {
  Unknown
  Healthy
  Degraded
  Unhealthy
} derive(Debug, Eq, ToJson, FromJson)

///|
/// Capabilities that a node can advertise to the control plane.
pub(all) enum NodeCapability {
  Telemetry
  CommandExecution
  PersistentStorage
} derive(Debug, Eq, ToJson, FromJson)

///|
/// A stable identifier for one edge node.
pub(all) struct NodeId {
  value : String
} derive(Debug, Eq, ToJson, FromJson)

///|
/// Creates a non-empty node identifier.
pub fn NodeId::new(value~ : String) -> NodeId raise NodeError {
  if value == "" {
    raise NodeError::EmptyNodeId
  }
  { value, }
}

///|
/// A non-negative Unix timestamp measured in seconds.
pub(all) struct Timestamp {
  unix_seconds : Int32
} derive(Debug, Eq, ToJson, FromJson)

///|
/// Creates a validated timestamp.
pub fn Timestamp::new(unix_seconds~ : Int32) -> Timestamp raise NodeError {
  if unix_seconds < 0 {
    raise NodeError::InvalidTimestamp
  }
  { unix_seconds, }
}

///|
/// Identity, capabilities, lifecycle, health, and observation time for a node.
pub(all) struct Node {
  id : NodeId
  capabilities : Array[NodeCapability]
  lifecycle : NodeLifecycle
  health : NodeHealth
  created_at : Timestamp
  updated_at : Timestamp
} derive(Debug, Eq, ToJson, FromJson)

///|
/// Creates a node in the New lifecycle with unknown health.
pub fn Node::new(
  id~ : String,
  capabilities~ : Array[NodeCapability],
  created_at~ : Timestamp,
) -> Node raise NodeError {
  {
    id: NodeId::new(value=id),
    capabilities,
    lifecycle: NodeLifecycle::New,
    health: NodeHealth::Unknown,
    created_at,
    updated_at: created_at,
  }
}

///|
/// Reports whether the node advertises a capability.
pub fn Node::supports(self : Node, capability~ : NodeCapability) -> Bool {
  for item in self.capabilities {
    if item == capability {
      return true
    }
  }
  false
}

///|
/// Applies a legal lifecycle transition at a monotonic timestamp.
pub fn Node::transition(
  self : Node,
  target~ : NodeLifecycle,
  at~ : Timestamp,
) -> Node raise NodeError {
  if at.unix_seconds < self.updated_at.unix_seconds {
    raise NodeError::InvalidTimestamp
  }
  if !can_transition(self.lifecycle, target) {
    raise NodeError::InvalidTransition("lifecycle transition is not allowed")
  }
  {
    id: self.id,
    capabilities: self.capabilities,
    lifecycle: target,
    health: self.health,
    created_at: self.created_at,
    updated_at: at,
  }
}

///|
/// Records a health observation at a monotonic timestamp.
pub fn Node::record_health(
  self : Node,
  health~ : NodeHealth,
  at~ : Timestamp,
) -> Node raise NodeError {
  if self.lifecycle == NodeLifecycle::Retired {
    raise NodeError::InvalidHealthUpdate
  }
  if at.unix_seconds < self.updated_at.unix_seconds {
    raise NodeError::InvalidTimestamp
  }
  {
    id: self.id,
    capabilities: self.capabilities,
    lifecycle: self.lifecycle,
    health,
    created_at: self.created_at,
    updated_at: at,
  }
}

///|
fn can_transition(from : NodeLifecycle, to : NodeLifecycle) -> Bool {
  match (from, to) {
    (NodeLifecycle::New, NodeLifecycle::Enrolled) => true
    (NodeLifecycle::New, NodeLifecycle::Retired) => true
    (NodeLifecycle::Enrolled, NodeLifecycle::Online) => true
    (NodeLifecycle::Enrolled, NodeLifecycle::Offline) => true
    (NodeLifecycle::Enrolled, NodeLifecycle::Retired) => true
    (NodeLifecycle::Online, NodeLifecycle::Offline) => true
    (NodeLifecycle::Online, NodeLifecycle::Retired) => true
    (NodeLifecycle::Offline, NodeLifecycle::Online) => true
    (NodeLifecycle::Offline, NodeLifecycle::Retired) => true
    _ => false
  }
}