///|
/// Validate a complete plan and return every deterministic diagnostic.
pub fn validate_plan(plan : EvacuationPlan) -> Array[Diagnostic] {
  let diagnostics : Array[Diagnostic] = []
  validate_plan_header(plan, diagnostics)
  validate_places(plan, diagnostics)
  validate_passages(plan, diagnostics)
  validate_populations(plan, diagnostics)
  validate_connectivity(plan, diagnostics)
  validate_exit_redundancy(plan, diagnostics)
  diagnostics
}

///|
fn validate_plan_header(
  plan : EvacuationPlan,
  diagnostics : Array[Diagnostic],
) -> Unit {
  if plan.name.length() == 0 {
    diagnostics.push(
      diagnostic(
        "PLAN_NAME_EMPTY",
        Error,
        "plan",
        "The evacuation plan has no name.",
        "Give the plan a short human-readable name.",
      ),
    )
  }
  if plan.timestep_seconds <= 0.0 {
    diagnostics.push(
      diagnostic(
        "TIMESTEP_INVALID",
        Error,
        "plan",
        "The simulation timestep must be positive.",
        "Use a timestep between 0.1 and 5 seconds.",
      ),
    )
  }
  if plan.places.length() == 0 {
    diagnostics.push(
      diagnostic(
        "PLAN_EMPTY",
        Error,
        "plan",
        "The plan contains no places.",
        "Add at least one occupied room and one exit.",
      ),
    )
  }
  if plan.place_kind_count(Exit) == 0 {
    diagnostics.push(
      diagnostic(
        "NO_EXIT",
        Error,
        "plan",
        "The plan contains no exit.",
        "Mark at least one place as an exit.",
      ),
    )
  }
}

///|
fn validate_places(
  plan : EvacuationPlan,
  diagnostics : Array[Diagnostic],
) -> Unit {
  for i = 0; i < plan.places.length(); i = i + 1 {
    let place = plan.places[i]
    if place.id.length() == 0 {
      diagnostics.push(
        diagnostic(
          "PLACE_ID_EMPTY",
          Error,
          "place[\{i}]",
          "A place has an empty identifier.",
          "Use a stable identifier such as room-a101.",
        ),
      )
    }
    if place.label.length() == 0 {
      diagnostics.push(
        diagnostic(
          "PLACE_LABEL_EMPTY",
          Warning,
          place.id,
          "A place has no display label.",
          "Add a label that responders can recognize.",
        ),
      )
    }
    if place.capacity <= 0 {
      diagnostics.push(
        diagnostic(
          "PLACE_CAPACITY_INVALID",
          Error,
          place.id,
          "The holding capacity must be positive.",
          "Set a conservative positive capacity.",
        ),
      )
    }
    if place.initial_risk < 0.0 || place.initial_risk > 1.0 {
      diagnostics.push(
        diagnostic(
          "PLACE_RISK_RANGE",
          Error,
          place.id,
          "Initial risk is outside the range 0 to 1.",
          "Clamp risk values to the inclusive range 0 to 1.",
        ),
      )
    }
    for j = i + 1; j < plan.places.length(); j = j + 1 {
      if plan.places[j].id == place.id {
        diagnostics.push(
          diagnostic(
            "PLACE_ID_DUPLICATE",
            Error,
            place.id,
            "Two places use the same identifier.",
            "Assign a unique identifier to every place.",
          ),
        )
      }
    }
  }
}

///|
fn validate_passages(
  plan : EvacuationPlan,
  diagnostics : Array[Diagnostic],
) -> Unit {
  for i = 0; i < plan.passages.length(); i = i + 1 {
    let passage = plan.passages[i]
    if passage.id.length() == 0 {
      diagnostics.push(
        diagnostic(
          "PASSAGE_ID_EMPTY",
          Error,
          "passage[\{i}]",
          "A passage has an empty identifier.",
          "Use a stable identifier such as door-a101-east.",
        ),
      )
    }
    if plan.place_index(passage.from_id) < 0 {
      diagnostics.push(
        diagnostic(
          "PASSAGE_FROM_UNKNOWN",
          Error,
          passage.id,
          "The passage starts at an unknown place.",
          "Correct from_id or add the missing place.",
        ),
      )
    }
    if plan.place_index(passage.to_id) < 0 {
      diagnostics.push(
        diagnostic(
          "PASSAGE_TO_UNKNOWN",
          Error,
          passage.id,
          "The passage ends at an unknown place.",
          "Correct to_id or add the missing place.",
        ),
      )
    }
    if passage.from_id == passage.to_id {
      diagnostics.push(
        diagnostic(
          "PASSAGE_SELF_LOOP",
          Warning,
          passage.id,
          "The passage connects a place to itself.",
          "Remove the self-loop unless it represents a deliberate delay.",
        ),
      )
    }
    if passage.length_m <= 0.0 {
      diagnostics.push(
        diagnostic(
          "PASSAGE_LENGTH_INVALID",
          Error,
          passage.id,
          "Passage length must be positive.",
          "Measure or estimate a positive travel length.",
        ),
      )
    }
    if passage.width_m <= 0.0 {
      diagnostics.push(
        diagnostic(
          "PASSAGE_WIDTH_INVALID",
          Error,
          passage.id,
          "Passage width must be positive.",
          "Measure the narrowest usable width.",
        ),
      )
    }
    if passage.flow_per_second <= 0.0 {
      diagnostics.push(
        diagnostic(
          "PASSAGE_FLOW_INVALID",
          Error,
          passage.id,
          "Passage flow rate must be positive.",
          "Use a conservative people-per-second rate.",
        ),
      )
    }
    if passage.kind == Stairway && passage.accessible {
      diagnostics.push(
        diagnostic(
          "STAIR_MARKED_ACCESSIBLE",
          Warning,
          passage.id,
          "A stairway is marked as accessible.",
          "Confirm an evacuation chair or accessible alternative exists.",
        ),
      )
    }
    for j = i + 1; j < plan.passages.length(); j = j + 1 {
      if plan.passages[j].id == passage.id {
        diagnostics.push(
          diagnostic(
            "PASSAGE_ID_DUPLICATE",
            Error,
            passage.id,
            "Two passages use the same identifier.",
            "Assign a unique identifier to every passage.",
          ),
        )
      }
    }
  }
}

///|
fn validate_populations(
  plan : EvacuationPlan,
  diagnostics : Array[Diagnostic],
) -> Unit {
  for i = 0; i < plan.populations.length(); i = i + 1 {
    let group = plan.populations[i]
    if group.id.length() == 0 {
      diagnostics.push(
        diagnostic(
          "GROUP_ID_EMPTY",
          Error,
          "population[\{i}]",
          "A population group has an empty identifier.",
          "Use a stable group identifier.",
        ),
      )
    }
    if plan.place_index(group.place_id) < 0 {
      diagnostics.push(
        diagnostic(
          "GROUP_PLACE_UNKNOWN",
          Error,
          group.id,
          "A population group starts at an unknown place.",
          "Correct place_id or add the missing place.",
        ),
      )
    }
    if group.count <= 0 {
      diagnostics.push(
        diagnostic(
          "GROUP_COUNT_INVALID",
          Error,
          group.id,
          "Population count must be positive.",
          "Remove empty groups or set a positive count.",
        ),
      )
    }
    if group.response_delay < 0 {
      diagnostics.push(
        diagnostic(
          "GROUP_DELAY_INVALID",
          Error,
          group.id,
          "Response delay cannot be negative.",
          "Use zero for immediate response.",
        ),
      )
    }
    if group.familiarity < 0.0 || group.familiarity > 1.0 {
      diagnostics.push(
        diagnostic(
          "GROUP_FAMILIARITY_RANGE",
          Error,
          group.id,
          "Familiarity is outside the range 0 to 1.",
          "Clamp familiarity to the inclusive range 0 to 1.",
        ),
      )
    }
  }
}

///|
fn direct_neighbors(plan : EvacuationPlan, place_index : Int) -> Array[Int] {
  let result : Array[Int] = []
  if place_index < 0 || place_index >= plan.places.length() {
    return result
  }
  let place_id = plan.places[place_index].id
  for passage in plan.passages {
    if passage.enabled && passage.from_id == place_id {
      let target = plan.place_index(passage.to_id)
      if target >= 0 {
        result.push(target)
      }
    }
    if passage.enabled && passage.bidirectional && passage.to_id == place_id {
      let target = plan.place_index(passage.from_id)
      if target >= 0 {
        result.push(target)
      }
    }
  }
  result
}

///|
fn can_reach_any_exit(plan : EvacuationPlan, start : Int) -> Bool {
  if start < 0 || start >= plan.places.length() {
    return false
  }
  let visited = Array::make(plan.places.length(), false)
  let queue : Array[Int] = [start]
  visited[start] = true
  let mut cursor = 0
  while cursor < queue.length() {
    let current = queue[cursor]
    cursor = cursor + 1
    if plan.places[current].kind == Exit {
      return true
    }
    for next in direct_neighbors(plan, current) {
      if !visited[next] {
        visited[next] = true
        queue.push(next)
      }
    }
  }
  false
}

///|
fn validate_connectivity(
  plan : EvacuationPlan,
  diagnostics : Array[Diagnostic],
) -> Unit {
  for i = 0; i < plan.places.length(); i = i + 1 {
    let place = plan.places[i]
    if place.kind != Exit && !can_reach_any_exit(plan, i) {
      diagnostics.push(
        diagnostic(
          "EXIT_UNREACHABLE",
          Error,
          place.id,
          "This place cannot reach an enabled exit.",
          "Add or enable a directed route to an exit.",
        ),
      )
    }
  }
}

///|
fn count_reachable_exits(plan : EvacuationPlan, start : Int) -> Int {
  if start < 0 || start >= plan.places.length() {
    return 0
  }
  let visited = Array::make(plan.places.length(), false)
  let queue : Array[Int] = [start]
  visited[start] = true
  let mut cursor = 0
  let mut exits = 0
  while cursor < queue.length() {
    let current = queue[cursor]
    cursor = cursor + 1
    if plan.places[current].kind == Exit {
      exits = exits + 1
    }
    for next in direct_neighbors(plan, current) {
      if !visited[next] {
        visited[next] = true
        queue.push(next)
      }
    }
  }
  exits
}

///|
fn validate_exit_redundancy(
  plan : EvacuationPlan,
  diagnostics : Array[Diagnostic],
) -> Unit {
  if plan.exit_indices().length() < 2 && plan.population_count() > 20 {
    diagnostics.push(
      diagnostic(
        "SINGLE_EXIT_GLOBAL",
        Warning,
        "plan",
        "A populated plan has fewer than two exits.",
        "Evaluate whether an independent alternative exit is required.",
      ),
    )
  }
  for group in plan.populations {
    let start = plan.place_index(group.place_id)
    if start >= 0 && count_reachable_exits(plan, start) == 1 && group.count > 10 {
      diagnostics.push(
        diagnostic(
          "SINGLE_EXIT_GROUP",
          Warning,
          group.id,
          "This group can reach only one exit.",
          "Add an independent route or document the accepted limitation.",
        ),
      )
    }
  }
}

///|
/// Count diagnostics of one severity.
pub fn diagnostic_count(
  diagnostics : Array[Diagnostic],
  severity : Severity,
) -> Int {
  let mut count = 0
  for item in diagnostics {
    if item.severity == severity {
      count = count + 1
    }
  }
  count
}

///|
/// Whether validation found no errors.
pub fn validation_ok(diagnostics : Array[Diagnostic]) -> Bool {
  diagnostic_count(diagnostics, Error) == 0
}