///|
pub(all) struct UtilityNode {
  name : String
  demand : Double
  capacity : Double
  priority : Int
  available : Bool
} derive(Debug, Eq, ToJson)

///|
pub(all) struct UtilityNetwork {
  kind : String
  nodes : Array[UtilityNode]
  total_demand : Double
  total_capacity : Double
  utilization : Double
  feasible : Bool
  curtailed_demand : Double
} derive(Debug, Eq, ToJson)

///|
pub(all) struct UtilityAllocation {
  node : String
  requested : Double
  allocated : Double
  curtailed : Double
  fraction : Double
  served : Bool
} derive(Debug, Eq, ToJson)

///|
pub fn utility_node(
  name : String,
  demand : Double,
  capacity : Double,
  priority : Int,
) -> UtilityNode {
  { name, demand, capacity, priority, available: capacity > 0.0 }
}

///|
pub fn build_utility_network(
  kind : String,
  nodes : ArrayView[UtilityNode],
) -> UtilityNetwork {
  let mut demand = 0.0
  let mut capacity = 0.0
  for node in nodes {
    demand += node.demand.max(0.0)
    capacity += node.capacity.max(0.0)
  }
  {
    kind,
    nodes: nodes.to_owned(),
    total_demand: demand,
    total_capacity: capacity,
    utilization: if capacity == 0.0 {
      0.0
    } else {
      demand / capacity
    },
    feasible: demand <= capacity,
    curtailed_demand: (demand - capacity).max(0.0),
  }
}

///|
pub fn network_utilization(network : UtilityNetwork) -> Double {
  network.utilization
}

///|
pub fn network_margin(network : UtilityNetwork) -> Double {
  network.total_capacity - network.total_demand
}

///|
pub fn network_is_feasible(
  network : UtilityNetwork,
  reserve_fraction : Double,
) -> Bool {
  network.total_demand <= network.total_capacity * (1.0 - reserve_fraction)
}

///|
pub fn network_reserve(network : UtilityNetwork) -> Double {
  if network.total_capacity == 0.0 {
    0.0
  } else {
    network_margin(network) / network.total_capacity
  }
}

///|
pub fn allocate_utility(network : UtilityNetwork) -> Array[UtilityAllocation] {
  let indices = [ for index in 0.. index ]
  for first in 0..= requested,
    })
  }
  result
}

///|
pub fn allocations_total(allocations : ArrayView[UtilityAllocation]) -> Double {
  allocations.fold(init=0.0, (sum, allocation) => sum + allocation.allocated)
}

///|
pub fn allocations_curtailed(
  allocations : ArrayView[UtilityAllocation],
) -> Double {
  allocations.fold(init=0.0, (sum, allocation) => sum + allocation.curtailed)
}

///|
pub fn allocations_service_rate(
  allocations : ArrayView[UtilityAllocation],
) -> Double {
  let mut requested = 0.0
  let mut allocated = 0.0
  for allocation in allocations {
    requested += allocation.requested
    allocated += allocation.allocated
  }
  if requested == 0.0 {
    1.0
  } else {
    allocated / requested
  }
}

///|
pub fn allocations_failed_nodes(
  allocations : ArrayView[UtilityAllocation],
) -> Array[String] {
  let result = []
  for allocation in allocations {
    if !allocation.served {
      result.push(allocation.node)
    }
  }
  result
}

///|
pub fn network_add_node(
  network : UtilityNetwork,
  node : UtilityNode,
) -> UtilityNetwork {
  let nodes = network.nodes.copy()
  nodes.push(node)
  build_utility_network(network.kind, nodes)
}

///|
pub fn network_remove_node(
  network : UtilityNetwork,
  name : String,
) -> UtilityNetwork {
  let nodes = []
  for node in network.nodes {
    if node.name != name {
      nodes.push(node)
    }
  }
  build_utility_network(network.kind, nodes)
}

///|
pub fn network_scale_demand(
  network : UtilityNetwork,
  factor : Double,
) -> UtilityNetwork {
  let nodes = [
    for node in network.nodes => {
      {
        name: node.name,
        demand: node.demand * factor,
        capacity: node.capacity,
        priority: node.priority,
        available: node.available,
      }
    }
  ]
  build_utility_network(network.kind, nodes)
}

///|
pub fn network_scale_capacity(
  network : UtilityNetwork,
  factor : Double,
) -> UtilityNetwork {
  let nodes = [
    for node in network.nodes => {
      {
        name: node.name,
        demand: node.demand,
        capacity: node.capacity * factor,
        priority: node.priority,
        available: node.available && factor > 0.0,
      }
    }
  ]
  build_utility_network(network.kind, nodes)
}

///|
pub fn network_sensitivity(
  network : UtilityNetwork,
  demand_factors : ArrayView[Double],
) -> Array[Double] {
  let result = []
  for factor in demand_factors {
    result.push(network_scale_demand(network, factor).utilization)
  }
  result
}

///|
pub fn utility_allocation_csv(
  allocations : ArrayView[UtilityAllocation],
) -> String {
  let lines = ["node,requested,allocated,curtailed,fraction,served"]
  for allocation in allocations {
    lines.push(
      allocation.node +
      "," +
      allocation.requested.to_string() +
      "," +
      allocation.allocated.to_string() +
      "," +
      allocation.curtailed.to_string() +
      "," +
      allocation.fraction.to_string() +
      "," +
      allocation.served.to_string(),
    )
  }
  lines.join("\n")
}

///|
pub fn utility_network_markdown(network : UtilityNetwork) -> String {
  let lines = [
    "# Utility network: " + network.kind,
    "",
    "| Node | Demand | Capacity | Priority | Available |",
    "|---|---:|---:|---:|---|",
  ]
  for node in network.nodes {
    lines.push(
      "| " +
      node.name +
      " | " +
      node.demand.to_string() +
      " | " +
      node.capacity.to_string() +
      " | " +
      node.priority.to_string() +
      " | " +
      node.available.to_string() +
      " |",
    )
  }
  lines.push("")
  lines.push("Utilization: " + network.utilization.to_string())
  lines.push("Feasible: " + network.feasible.to_string())
  lines.join("\n")
}

///|
pub fn utility_network_summary(network : UtilityNetwork) -> String {
  "kind=" +
  network.kind +
  ",nodes=" +
  network.nodes.length().to_string() +
  ",demand=" +
  network.total_demand.to_string() +
  ",capacity=" +
  network.total_capacity.to_string() +
  ",utilization=" +
  network.utilization.to_string() +
  ",curtailed=" +
  network.curtailed_demand.to_string()
}

///|
pub fn utility_node_rank(network : UtilityNetwork, name : String) -> Int {
  for node in network.nodes {
    if node.name == name {
      return node.priority
    }
  }
  -1
}

///|
pub fn utility_network_max_priority(network : UtilityNetwork) -> Int {
  let mut result = 0
  for node in network.nodes {
    result = result.max(node.priority)
  }
  result
}

///|
pub fn utility_network_min_priority(network : UtilityNetwork) -> Int {
  if network.nodes.length() == 0 {
    return 0
  }
  let mut result = network.nodes[0].priority
  for node in network.nodes {
    result = result.min(node.priority)
  }
  result
}

///|
pub fn utility_node_count_available(network : UtilityNetwork) -> Int {
  let mut count = 0
  for node in network.nodes {
    if node.available {
      count += 1
    }
  }
  count
}

///|
pub fn utility_network_alerts(
  network : UtilityNetwork,
  maximum_utilization : Double,
) -> Array[String] {
  let alerts = []
  if network.utilization > maximum_utilization {
    alerts.push("network utilization exceeds limit")
  }
  if network.curtailed_demand > 0.0 {
    alerts.push("utility demand is curtailed")
  }
  if utility_node_count_available(network) < network.nodes.length() {
    alerts.push("one or more utility sources are unavailable")
  }
  alerts
}

///|
pub fn utility_network_balanced(
  network : UtilityNetwork,
  tolerance : Double,
) -> Bool {
  if network.nodes.length() == 0 {
    return true
  }
  let average = network.total_capacity / network.nodes.length().to_double()
  for node in network.nodes {
    if (node.capacity - average).abs() > tolerance * average.max(1.0) {
      return false
    }
  }
  true
}

///|
pub fn utility_network_cost(
  network : UtilityNetwork,
  unit_cost : Double,
) -> Double {
  network.total_demand * unit_cost
}

///|
pub fn utility_network_emissions(
  network : UtilityNetwork,
  emission_factor : Double,
) -> Double {
  network.total_demand * emission_factor
}