///|
pub suberror GraphBuildError {
  DuplicateNode(NodeId)
  DuplicateRouter(NodeId)
  InvalidId(String)
} derive(Debug)

///|
pub suberror GraphValidationError {
  MissingEntry
  UnknownEntry(NodeId)
  MissingRouter(NodeId)
  UnknownRouterSource(NodeId)
  UnknownDestination(from~ : NodeId, to~ : NodeId)
  UnreachableNode(NodeId)
} derive(Debug)

///|
pub struct RunFailure {
  primary : Error
  cleanup : ReadOnlyArray[Error]
} derive(Debug)

///|
pub suberror GraphRuntimeError {
  NodeTimedOut(node_id~ : NodeId, step~ : Int, timeout_ms~ : Int)
  NodeFailed(node_id~ : NodeId, step~ : Int, cause~ : Error)
  ReduceFailed(node_id~ : NodeId, step~ : Int, cause~ : Error)
  RouteFailed(node_id~ : NodeId, step~ : Int, cause~ : Error)
  RouteContractViolated(from~ : NodeId, to~ : NodeId)
  StepLimitExceeded(limit~ : Int)
  ExplicitFailure(node_id~ : NodeId, message~ : String)
  ResourceCleanupFailed(failure~ : RunFailure)
  NodeNotFound(NodeId)
  RouterNotFound(NodeId)
} derive(Debug)

///|
pub struct GraphDefinition[S, P] {
  reducer : Reducer[S, P]
  nodes : Map[NodeId, Node[S, P]]
  routers : Map[NodeId, Router[S]]
  mut entry : NodeId?
}

///|
pub fn[S, P] GraphDefinition::GraphDefinition(
  reducer : Reducer[S, P],
) -> GraphDefinition[S, P] {
  { reducer, nodes: Map([]), routers: Map([]), entry: None }
}

///|
pub fn[S, P] GraphDefinition::add_node(
  self : GraphDefinition[S, P],
  node : Node[S, P],
) -> Unit raise GraphBuildError {
  guard !self.nodes.contains(node.id) else { raise DuplicateNode(node.id) }
  self.nodes[node.id] = node
}

///|
pub fn[S, P] GraphDefinition::set_router(
  self : GraphDefinition[S, P],
  from : NodeId,
  value : Router[S],
) -> Unit raise GraphBuildError {
  guard !self.routers.contains(from) else { raise DuplicateRouter(from) }
  self.routers[from] = value
}

///|
pub fn[S, P] GraphDefinition::set_entry(
  self : GraphDefinition[S, P],
  entry : NodeId,
) -> Unit raise GraphBuildError {
  guard !entry.to_string().is_empty() else { raise InvalidId("NodeId") }
  self.entry = Some(entry)
}

///|
fn[S, P] validate_graph(
  nodes : Map[NodeId, Node[S, P]],
  routers : Map[NodeId, Router[S]],
  entry : NodeId,
) -> Unit raise GraphValidationError {
  guard nodes.contains(entry) else { raise UnknownEntry(entry) }
  for source in routers.keys() {
    guard nodes.contains(source) else { raise UnknownRouterSource(source) }
  }
  for node_id in nodes.keys() {
    guard routers.contains(node_id) else { raise MissingRouter(node_id) }
  }
  for source, value in routers {
    for route in value.declared_routes {
      guard nodes.contains(route.target) else {
        raise UnknownDestination(from=source, to=route.target)
      }
    }
  }
  let reachable : Set[NodeId] = Set::default()
  let pending = [entry]
  while pending.pop() is Some(node_id) {
    if reachable.add_and_check(node_id) {
      let value = routers[node_id]
      for route in value.declared_routes {
        if !reachable.contains(route.target) {
          pending.push(route.target)
        }
      }
    }
  }
  for node_id in nodes.keys() {
    guard reachable.contains(node_id) else { raise UnreachableNode(node_id) }
  }
}

///|
pub fn[S, P] GraphDefinition::compile(
  self : GraphDefinition[S, P],
) -> CompiledGraph[S, P] raise GraphValidationError {
  let entry = self.entry.unwrap_or_error(MissingEntry)
  validate_graph(self.nodes, self.routers, entry)
  let nodes = @immut_hashmap.from_iter(self.nodes.iter())
  let routers = @immut_hashmap.from_iter(self.routers.iter())
  CompiledGraph::{ reducer: self.reducer, nodes, routers, entry }
}

///|
pub struct CompiledGraph[S, P] {
  reducer : Reducer[S, P]
  nodes : @immut_hashmap.HashMap[NodeId, Node[S, P]]
  routers : @immut_hashmap.HashMap[NodeId, Router[S]]
  entry : NodeId
}

///|
/// Describes one node in a compiled graph without exposing its callbacks.
pub struct CompiledNodeSnapshot {
  id : NodeId
  metadata : NodeMetadata
  router_metadata : RouterMetadata
  declared_routes : ReadOnlyArray[DeclaredRoute]
} derive(Debug)

///|
/// Contains the callback-free static structure of a compiled graph.
pub struct CompiledGraphSnapshot {
  entry : NodeId
  nodes : ReadOnlyArray[CompiledNodeSnapshot]
} derive(Debug)

///|
pub fn[S, P] CompiledGraph::entry(self : CompiledGraph[S, P]) -> NodeId {
  self.entry
}

///|
pub fn[S, P] CompiledGraph::get_node(
  self : CompiledGraph[S, P],
  id : NodeId,
) -> Node[S, P] raise GraphRuntimeError {
  self.nodes.get(id).unwrap_or_error(NodeNotFound(id))
}

///|
pub fn[S, P] CompiledGraph::get_router(
  self : CompiledGraph[S, P],
  id : NodeId,
) -> Router[S] raise GraphRuntimeError {
  self.routers.get(id).unwrap_or_error(RouterNotFound(id))
}

///|
/// Returns a deterministic, callback-free snapshot for inspection and tooling.
pub fn[S, P] CompiledGraph::snapshot(
  self : CompiledGraph[S, P],
) -> CompiledGraphSnapshot {
  let node_ids = self.nodes.keys().to_array()
  node_ids.sort_by((left, right) => left.to_string().compare(right.to_string()))
  let nodes = node_ids.map(node_id => {
    let node = self.nodes[node_id]
    let router = self.routers[node_id]
    let routes = [..router.declared_routes]
    routes.sort_by((left, right) => {
      left.target.to_string().compare(right.target.to_string())
    })
    CompiledNodeSnapshot::{
      id: node_id,
      metadata: node.metadata,
      router_metadata: router.metadata,
      declared_routes: ReadOnlyArray::from_array(routes),
    }
  })
  CompiledGraphSnapshot::{
    entry: self.entry,
    nodes: ReadOnlyArray::from_array(nodes),
  }
}