///|
fn ssa_variable_names_json(
  program : FlowProgram,
  indices : Array[Int32],
) -> Json {
  let ordered = indices.copy()
  ordered.sort()
  Json::array(
    ordered.map(variable => Json::string(program.variables[variable])),
  )
}

///|
/// Internal serializer for a fresh, valid plan produced by plan_ssa.
fn ssa_plan_json_value(plan : SsaPlan) -> Json {
  let program = plan.program
  let graph = program.graph
  let phi_variables : Array[Array[Int32]] = Array::makei(graph.nodes.length(), _ => {
    []
  })
  let entry_values_required = []
  for placement in plan.placements {
    for node in placement.phi_nodes {
      phi_variables[node].push(placement.variable)
    }
    if placement.requires_entry_value {
      entry_values_required.push(
        Json::string(program.variables[placement.variable]),
      )
    }
  }
  let blocks = []
  for node, name in graph.nodes {
    blocks.push(
      Json::object(
        Map([
          ("id", Json::string(name)),
          ("reachable", Json::boolean(plan.liveness.reachable[node])),
          (
            "defines",
            ssa_variable_names_json(program, program.definitions[node]),
          ),
          ("uses", ssa_variable_names_json(program, program.uses[node])),
          (
            "live_in",
            ssa_variable_names_json(program, plan.liveness.live_in[node]),
          ),
          (
            "live_out",
            ssa_variable_names_json(program, plan.liveness.live_out[node]),
          ),
          (
            "phi_variables",
            ssa_variable_names_json(program, phi_variables[node]),
          ),
        ]),
      ),
    )
  }
  let placements = plan.placements.map(placement => {
    Json::object(
      Map([
        ("variable", Json::string(program.variables[placement.variable])),
        ("definition_blocks", names_json(graph, placement.definitions)),
        ("live_in_blocks", names_json(graph, placement.live_in)),
        ("phi_blocks", names_json(graph, placement.phi_nodes)),
        ("requires_entry_value", Json::boolean(placement.requires_entry_value)),
      ]),
    )
  })
  Json::object(
    Map([
      ("schema", Json::string("cairndom.ssa-plan.v1")),
      ("entry", Json::string(graph.nodes[graph.entry])),
      ("scope", Json::string("entry_reachable_all_paths")),
      ("use_convention", Json::string("before_local_definition")),
      ("status", Json::string("locations_only")),
      (
        "variables",
        Json::array(program.variables.map(name => Json::string(name))),
      ),
      ("blocks", Json::array(blocks)),
      ("placements", Json::array(placements)),
      ("entry_values_required", Json::array(entry_values_required)),
      (
        "entry_value_semantics",
        Json::string(
          "Variables live before entry local definitions require a caller-supplied parameter or initial value, or a missing-initialization diagnostic under the source language's rules.",
        ),
      ),
    ]),
  )
}

///|
/// Compute a fresh plan and serialize cairndom.ssa-plan.v1 using original IDs.
/// The report contains phi locations and entry-value requirements, without IR
/// rewriting or variable renaming. Unreachable input facts remain visible but
/// contribute no liveness or placement results. Output is limited to 16 MiB.
pub fn ssa_plan_json_text(program : FlowProgram) -> String raise GraphError {
  let plan = plan_ssa(program)
  let output = ssa_plan_json_value(plan).stringify(indent=2)
  if output.length() > 16_777_216 {
    raise Invalid("SSA plan JSON exceeds 16 MiB")
  }
  output
}