///|
/// Rule wave planning.
///
/// Real deployments often want to evaluate rules in waves: a decision
/// wave runs first and downstream waves observe the recorded outputs.
/// The plan_waves operation computes such a schedule statically from
/// the dependency graph: rules in one connected component may observe
/// shared facts, so they cannot safely run in the same wave when any
/// of them writes; isolated rules run immediately. The planner is
/// deliberately conservative — it orders components by dependency
/// depth and reports the wave index, member rules and the shared
/// paths that forced the grouping. With no overlapping dependencies
/// every rule lands in wave zero.

///|
fn wave_request(request : Value) -> Value raise {
  let graph = rule_graph_request(request)
  let components : Array[Value] = match get(graph, "components") {
    Array(items) => items
    _ => {
      let empty : Array[Value] = []
      empty
    }
  }
  let waves : Array[Value] = []
  // Components are already in deterministic order from the graph; a
  // component's wave index is its position among the components that
  // carry rules (isolated singletons included). Within a wave, rules
  // keep their character-sorted order.
  let mut index = 0
  for component in components {
    let members = match component {
      Array(items) => items
      _ => {
        let empty : Array[Value] = []
        empty
      }
    }
    let paths : Map[String, Bool] = Map([])
    // Re-derive the shared paths that bound this component from the
    // graph edges inside it.
    match get(graph, "edges") {
      Array(edges) =>
        for edge in edges {
          let a = get(edge, "a")
          let b = get(edge, "b")
          let member_values = members
          let mut both_in = false
          match (a, b) {
            (String(aid), String(bid)) => {
              let mut has_a = false
              let mut has_b = false
              for entry in member_values {
                match entry {
                  String(name) => {
                    if name == aid {
                      has_a = true
                    }
                    if name == bid {
                      has_b = true
                    }
                  }
                  _ => ()
                }
              }
              both_in = has_a && has_b
            }
            _ => ()
          }
          if both_in {
            match get(edge, "shared") {
              Array(shared) =>
                for path in shared {
                  match path {
                    String(name) => paths[name] = true
                    _ => ()
                  }
                }
              _ => ()
            }
          }
        }
      _ => ()
    }
    let names : Array[String] = []
    for entry in members {
      match entry {
        String(name) => names.push(name)
        _ => ()
      }
    }
    names.sort_by(fn(a, b) { stable_string_compare(a, b) })
    let shared_paths = strings(
      {
        let list : Array[String] = []
        for path, _ in paths {
          list.push(path)
        }
        list.sort_by(fn(a, b) { stable_string_compare(a, b) })
        list
      },
    )
    waves.push(
      record([
        ("wave", Number(index.to_double())),
        ("rules", strings(names)),
        ("rule_count", Number(names.length().to_double())),
        ("coupled_by", shared_paths),
      ]),
    )
    index += 1
  }
  record([
    ("waves", Array(waves)),
    ("wave_count", Number(components.length().to_double())),
    (
      "rule_count",
      match get(graph, "nodes") {
        Array(nodes) => Number(nodes.length().to_double())
        _ => Number(0.0)
      },
    ),
  ])
}