///|
/// Settings controlling a complete evacuation run.
pub struct SimulationConfig {
  maximum_steps : Int
  reroute_interval : Int
  snapshot_interval : Int
  maximum_route_risk : Double
  stop_when_evacuated : Bool
  hazard : HazardConfig
} derive(Eq, Debug)

///|
/// A movable cohort. Cohorts split when passage capacity is insufficient.
struct Cohort {
  group_index : Int
  mut count : Int
  mut place_index : Int
  mut waiting_steps : Int
  mut stalled_steps : Int
  mut exposure : Double
  mut last_route_step : Int
  mut route : Route
} derive(Eq, Debug)

///|
/// Aggregate state captured for reports and regression tests.
pub struct SimulationSnapshot {
  step : Int
  remaining : Int
  evacuated : Int
  stalled : Int
  maximum_risk : Double
  mean_risk : Double
  passage_queues : Array[Int]
  place_occupancy : Array[Int]
} derive(Eq, Debug)

///|
/// Final group-level metrics.
pub struct GroupOutcome {
  group_id : String
  initial_count : Int
  evacuated_count : Int
  remaining_count : Int
  first_exit_step : Int
  last_exit_step : Int
  accumulated_exposure : Double
  reroutes : Int
} derive(Eq, Debug)

///|
/// Complete deterministic result.
pub struct SimulationResult {
  plan_name : String
  completed : Bool
  steps : Int
  total_people : Int
  evacuated_people : Int
  remaining_people : Int
  peak_stalled_people : Int
  peak_risk : Double
  mean_evacuation_step : Double
  snapshots : Array[SimulationSnapshot]
  group_outcomes : Array[GroupOutcome]
  passage_peak_queues : Array[Int]
  event_log : Array[String]
} derive(Eq, Debug)

///|
/// A state-aware edge used internally by the simulation router.
struct LiveEdge {
  passage_index : Int
  to_index : Int
  cost : Double
} derive(Eq, Debug)

///|
/// Practical deterministic defaults.
pub fn SimulationConfig::default() -> SimulationConfig {
  {
    maximum_steps: 600,
    reroute_interval: 5,
    snapshot_interval: 10,
    maximum_route_risk: 0.85,
    stop_when_evacuated: true,
    hazard: HazardConfig::default(),
  }
}

///|
/// Short configuration useful for tests.
pub fn SimulationConfig::short_run() -> SimulationConfig {
  {
    maximum_steps: 120,
    reroute_interval: 2,
    snapshot_interval: 1,
    maximum_route_risk: 0.9,
    stop_when_evacuated: true,
    hazard: HazardConfig::slow(),
  }
}

///|
fn int_min(a : Int, b : Int) -> Int {
  if a < b {
    a
  } else {
    b
  }
}

///|
fn int_max(a : Int, b : Int) -> Int {
  if a > b {
    a
  } else {
    b
  }
}

///|
fn live_edge_cost(
  plan : EvacuationPlan,
  state : ScenarioState,
  passage_index : Int,
  from_index : Int,
  to_index : Int,
  queues : Array[Int],
  mobility : Mobility,
) -> Double {
  let passage = plan.passages[passage_index]
  let risk = if to_index < state.risks.length() {
    state.risks[to_index]
  } else {
    0.0
  }
  let queue = if passage_index < queues.length() {
    queues[passage_index]
  } else {
    0
  }
  let queue_cost = queue.to_double() /
    (if passage.flow_per_second > 0.1 { passage.flow_per_second } else { 0.1 })
  let accessibility_cost = if mobility.requires_accessible() &&
    !passage.accessible {
    1000000000.0
  } else {
    0.0
  }
  let stair_cost = if passage.kind == Stairway {
    8.0 / mobility.speed_factor()
  } else {
    0.0
  }
  let direction_cost = if plan.places[from_index].floor !=
    plan.places[to_index].floor {
    2.0
  } else {
    0.0
  }
  passage.length_m / mobility.speed_factor() +
  risk * 100.0 +
  queue_cost * 5.0 +
  accessibility_cost +
  stair_cost +
  direction_cost
}

///|
fn live_outgoing_edges(
  plan : EvacuationPlan,
  state : ScenarioState,
  from_index : Int,
  queues : Array[Int],
  mobility : Mobility,
  maximum_risk : Double,
) -> Array[LiveEdge] {
  let result : Array[LiveEdge] = []
  if from_index < 0 || from_index >= plan.places.length() {
    return result
  }
  let from_id = plan.places[from_index].id
  for i = 0; i < plan.passages.length(); i = i + 1 {
    let passage = plan.passages[i]
    let active = passage.enabled &&
      (i >= state.passage_enabled.length() || state.passage_enabled[i])
    if !active {
      continue
    }
    if mobility.requires_accessible() && !passage.accessible {
      continue
    }
    if passage.from_id == from_id {
      let target = plan.place_index(passage.to_id)
      if target >= 0 &&
        (target >= state.risks.length() || state.risks[target] <= maximum_risk) {
        result.push({
          passage_index: i,
          to_index: target,
          cost: live_edge_cost(
            plan, state, i, from_index, target, queues, mobility,
          ),
        })
      }
    }
    if passage.bidirectional && passage.to_id == from_id {
      let target = plan.place_index(passage.from_id)
      if target >= 0 &&
        (target >= state.risks.length() || state.risks[target] <= maximum_risk) {
        result.push({
          passage_index: i,
          to_index: target,
          cost: live_edge_cost(
            plan, state, i, from_index, target, queues, mobility,
          ),
        })
      }
    }
  }
  result
}

///|
fn live_minimum(distance : Array[Double], visited : Array[Bool]) -> Int {
  let mut selected = -1
  let mut value = 1000000000000.0
  for i = 0; i < distance.length(); i = i + 1 {
    if !visited[i] && distance[i] < value {
      selected = i
      value = distance[i]
    }
  }
  selected
}

///|
fn rebuild_live_route(
  plan : EvacuationPlan,
  start : Int,
  target : Int,
  distance : Array[Double],
  previous_place : Array[Int],
  previous_passage : Array[Int],
  risks : Array[Double],
) -> Route {
  if target < 0 ||
    target >= distance.length() ||
    distance[target] >= 999999999999.0 {
    return Route::no_route()
  }
  let reverse_places : Array[Int] = []
  let reverse_passages : Array[Int] = []
  let mut cursor = target
  let mut hops = 0
  while cursor >= 0 && hops <= plan.places.length() {
    reverse_places.push(cursor)
    if cursor == start {
      break
    }
    let passage = previous_passage[cursor]
    if passage < 0 {
      return Route::no_route()
    }
    reverse_passages.push(passage)
    cursor = previous_place[cursor]
    hops = hops + 1
  }
  if reverse_places.length() == 0 ||
    reverse_places[reverse_places.length() - 1] != start {
    return Route::no_route()
  }
  let places : Array[Int] = []
  let passages : Array[Int] = []
  let mut route_distance = 0.0
  let mut route_risk = 0.0
  for i = reverse_places.length() - 1; i >= 0; i = i - 1 {
    let index = reverse_places[i]
    places.push(index)
    if index < risks.length() && risks[index] > route_risk {
      route_risk = risks[index]
    }
  }
  for i = reverse_passages.length() - 1; i >= 0; i = i - 1 {
    let index = reverse_passages[i]
    passages.push(index)
    route_distance = route_distance + plan.passages[index].length_m
  }
  {
    place_indices: places,
    passage_indices: passages,
    total_cost: distance[target],
    distance_m: route_distance,
    maximum_risk: route_risk,
    reachable: true,
  }
}

///|
fn live_route_to_exit(
  plan : EvacuationPlan,
  state : ScenarioState,
  start : Int,
  queues : Array[Int],
  mobility : Mobility,
  maximum_risk : Double,
) -> Route {
  let n = plan.places.length()
  if start < 0 || start >= n {
    return Route::no_route()
  }
  let distance = Array::make(n, 1000000000000.0)
  let visited = Array::make(n, false)
  let previous_place = Array::make(n, -1)
  let previous_passage = Array::make(n, -1)
  distance[start] = 0.0
  let mut target = -1
  for iteration = 0; iteration < n; iteration = iteration + 1 {
    let current = live_minimum(distance, visited)
    if current < 0 {
      break
    }
    if plan.places[current].kind == Exit {
      target = current
      break
    }
    visited[current] = true
    for
      edge in live_outgoing_edges(
        plan, state, current, queues, mobility, maximum_risk,
      ) {
      let candidate = distance[current] + edge.cost
      if candidate < distance[edge.to_index] {
        distance[edge.to_index] = candidate
        previous_place[edge.to_index] = current
        previous_passage[edge.to_index] = edge.passage_index
      }
    }
  }
  rebuild_live_route(
    plan,
    start,
    target,
    distance,
    previous_place,
    previous_passage,
    state.risks,
  )
}

///|
fn make_cohorts(plan : EvacuationPlan) -> Array[Cohort] {
  let result : Array[Cohort] = []
  for i = 0; i < plan.populations.length(); i = i + 1 {
    let group = plan.populations[i]
    let start = plan.place_index(group.place_id)
    result.push({
      group_index: i,
      count: group.count,
      place_index: start,
      waiting_steps: group.response_delay,
      stalled_steps: 0,
      exposure: 0.0,
      last_route_step: -100000,
      route: Route::no_route(),
    })
  }
  result
}

///|
fn cohort_people(cohorts : Array[Cohort]) -> Int {
  let mut total = 0
  for cohort in cohorts {
    if cohort.count > 0 {
      total = total + cohort.count
    }
  }
  total
}

///|
fn place_occupancy(
  plan : EvacuationPlan,
  cohorts : Array[Cohort],
) -> Array[Int] {
  let result = Array::make(plan.places.length(), 0)
  for cohort in cohorts {
    if cohort.count > 0 &&
      cohort.place_index >= 0 &&
      cohort.place_index < result.length() {
      result[cohort.place_index] = result[cohort.place_index] + cohort.count
    }
  }
  result
}

///|
fn desired_passage(
  plan : EvacuationPlan,
  state : ScenarioState,
  cohort : Cohort,
  queues : Array[Int],
  step : Int,
  config : SimulationConfig,
) -> Int {
  if cohort.place_index < 0 || cohort.place_index >= plan.places.length() {
    return -1
  }
  if plan.places[cohort.place_index].kind == Exit {
    return -1
  }
  let group = plan.populations[cohort.group_index]
  let route_expired = step - cohort.last_route_step >= config.reroute_interval
  let route_invalid = !cohort.route.reachable ||
    cohort.route.passage_indices.length() == 0
  if route_expired || route_invalid {
    cohort.route = live_route_to_exit(
      plan,
      state,
      cohort.place_index,
      queues,
      group.mobility,
      config.maximum_route_risk,
    )
    cohort.last_route_step = step
  }
  if cohort.route.reachable && cohort.route.passage_indices.length() > 0 {
    cohort.route.passage_indices[0]
  } else {
    -1
  }
}

///|
fn route_next_place(
  plan : EvacuationPlan,
  passage_index : Int,
  current : Int,
) -> Int {
  if passage_index < 0 ||
    passage_index >= plan.passages.length() ||
    current < 0 ||
    current >= plan.places.length() {
    return -1
  }
  let passage = plan.passages[passage_index]
  let current_id = plan.places[current].id
  if passage.from_id == current_id {
    plan.place_index(passage.to_id)
  } else if passage.bidirectional && passage.to_id == current_id {
    plan.place_index(passage.from_id)
  } else {
    -1
  }
}

///|
fn passage_step_capacity(
  plan : EvacuationPlan,
  timestep : Double,
) -> Array[Int] {
  let result : Array[Int] = []
  for passage in plan.passages {
    let raw = (passage.flow_per_second * timestep).to_int()
    result.push(int_max(1, raw))
  }
  result
}

///|
fn build_queues(
  plan : EvacuationPlan,
  state : ScenarioState,
  cohorts : Array[Cohort],
  step : Int,
  config : SimulationConfig,
) -> Array[Int] {
  let queues = Array::make(plan.passages.length(), 0)
  for cohort in cohorts {
    if cohort.count > 0 && cohort.waiting_steps <= 0 {
      let passage = desired_passage(plan, state, cohort, queues, step, config)
      if passage >= 0 && passage < queues.length() {
        queues[passage] = queues[passage] + cohort.count
      }
    }
  }
  queues
}

///|
fn copy_int_array(values : Array[Int]) -> Array[Int] {
  let result : Array[Int] = []
  for value in values {
    result.push(value)
  }
  result
}

///|
fn record_snapshot(
  plan : EvacuationPlan,
  state : ScenarioState,
  cohorts : Array[Cohort],
  queues : Array[Int],
  total_people : Int,
) -> SimulationSnapshot {
  let remaining = cohort_people(cohorts)
  let mut stalled = 0
  for cohort in cohorts {
    if cohort.stalled_steps > 0 {
      stalled = stalled + cohort.count
    }
  }
  let risk = observe_risk(state)
  {
    step: state.step,
    remaining,
    evacuated: total_people - remaining,
    stalled,
    maximum_risk: risk.maximum_risk,
    mean_risk: risk.mean_risk,
    passage_queues: copy_int_array(queues),
    place_occupancy: place_occupancy(plan, cohorts),
  }
}

///|
fn advance_cohorts(
  plan : EvacuationPlan,
  state : ScenarioState,
  cohorts : Array[Cohort],
  queues : Array[Int],
  capacity : Array[Int],
  step : Int,
  config : SimulationConfig,
  group_first_exit : Array[Int],
  group_last_exit : Array[Int],
  group_evacuated : Array[Int],
  group_reroutes : Array[Int],
) -> Unit {
  let initial_length = cohorts.length()
  for i = 0; i < initial_length; i = i + 1 {
    let cohort = cohorts[i]
    if cohort.count <= 0 {
      continue
    }
    if cohort.place_index < 0 || cohort.place_index >= plan.places.length() {
      cohort.stalled_steps = cohort.stalled_steps + 1
      continue
    }
    let risk = if cohort.place_index < state.risks.length() {
      state.risks[cohort.place_index]
    } else {
      0.0
    }
    cohort.exposure = cohort.exposure +
      risk * config.hazard.spread_rate * cohort.count.to_double()
    if plan.places[cohort.place_index].kind == Exit {
      let group_index = cohort.group_index
      group_evacuated[group_index] = group_evacuated[group_index] + cohort.count
      if group_first_exit[group_index] < 0 {
        group_first_exit[group_index] = step
      }
      group_last_exit[group_index] = step
      cohort.count = 0
      continue
    }
    if cohort.waiting_steps > 0 {
      cohort.waiting_steps = cohort.waiting_steps - 1
      continue
    }
    let previous_route_step = cohort.last_route_step
    let passage = desired_passage(plan, state, cohort, queues, step, config)
    if cohort.last_route_step != previous_route_step &&
      previous_route_step > -100000 {
      group_reroutes[cohort.group_index] = group_reroutes[cohort.group_index] +
        1
    }
    if passage < 0 || passage >= capacity.length() {
      cohort.stalled_steps = cohort.stalled_steps + 1
      continue
    }
    let next_place = route_next_place(plan, passage, cohort.place_index)
    if next_place < 0 {
      cohort.stalled_steps = cohort.stalled_steps + 1
      cohort.route = Route::no_route()
      continue
    }
    let allowed = int_min(cohort.count, capacity[passage])
    if allowed <= 0 {
      cohort.stalled_steps = cohort.stalled_steps + 1
      continue
    }
    capacity[passage] = capacity[passage] - allowed
    if allowed == cohort.count {
      cohort.place_index = next_place
      cohort.stalled_steps = 0
      cohort.route = Route::no_route()
    } else {
      cohort.count = cohort.count - allowed
      cohort.stalled_steps = cohort.stalled_steps + 1
      cohorts.push({
        group_index: cohort.group_index,
        count: allowed,
        place_index: next_place,
        waiting_steps: 0,
        stalled_steps: 0,
        exposure: cohort.exposure *
        allowed.to_double() /
        (cohort.count + allowed).to_double(),
        last_route_step: -100000,
        route: Route::no_route(),
      })
    }
  }
}

///|
fn update_peak_queues(current : Array[Int], peaks : Array[Int]) -> Unit {
  let count = int_min(current.length(), peaks.length())
  for i = 0; i < count; i = i + 1 {
    if current[i] > peaks[i] {
      peaks[i] = current[i]
    }
  }
}

///|
fn build_group_outcomes(
  plan : EvacuationPlan,
  cohorts : Array[Cohort],
  first_exit : Array[Int],
  last_exit : Array[Int],
  evacuated : Array[Int],
  reroutes : Array[Int],
) -> Array[GroupOutcome] {
  let result : Array[GroupOutcome] = []
  let remaining = Array::make(plan.populations.length(), 0)
  let exposures = Array::make(plan.populations.length(), 0.0)
  for cohort in cohorts {
    if cohort.group_index >= 0 && cohort.group_index < remaining.length() {
      remaining[cohort.group_index] = remaining[cohort.group_index] +
        cohort.count
      exposures[cohort.group_index] = exposures[cohort.group_index] +
        cohort.exposure
    }
  }
  for i = 0; i < plan.populations.length(); i = i + 1 {
    let group = plan.populations[i]
    result.push({
      group_id: group.id,
      initial_count: group.count,
      evacuated_count: evacuated[i],
      remaining_count: remaining[i],
      first_exit_step: first_exit[i],
      last_exit_step: last_exit[i],
      accumulated_exposure: exposures[i],
      reroutes: reroutes[i],
    })
  }
  result
}

///|
fn mean_evacuation_step(outcomes : Array[GroupOutcome]) -> Double {
  let mut weighted = 0.0
  let mut evacuated = 0
  for outcome in outcomes {
    if outcome.evacuated_count > 0 && outcome.last_exit_step >= 0 {
      weighted = weighted +
        outcome.last_exit_step.to_double() * outcome.evacuated_count.to_double()
      evacuated = evacuated + outcome.evacuated_count
    }
  }
  if evacuated > 0 {
    weighted / evacuated.to_double()
  } else {
    0.0
  }
}

///|
/// Run a complete deterministic evacuation simulation.
pub fn run_simulation(
  plan : EvacuationPlan,
  config : SimulationConfig,
) -> SimulationResult {
  let validation = validate_plan(plan)
  if !validation_ok(validation) {
    return {
      plan_name: plan.name,
      completed: false,
      steps: 0,
      total_people: plan.population_count(),
      evacuated_people: 0,
      remaining_people: plan.population_count(),
      peak_stalled_people: plan.population_count(),
      peak_risk: 0.0,
      mean_evacuation_step: 0.0,
      snapshots: [],
      group_outcomes: [],
      passage_peak_queues: Array::make(plan.passages.length(), 0),
      event_log: ["simulation rejected because plan validation failed"],
    }
  }
  let state = ScenarioState::from_plan(plan)
  let cohorts = make_cohorts(plan)
  let total_people = plan.population_count()
  let snapshots : Array[SimulationSnapshot] = []
  let peak_queues = Array::make(plan.passages.length(), 0)
  let first_exit = Array::make(plan.populations.length(), -1)
  let last_exit = Array::make(plan.populations.length(), -1)
  let evacuated = Array::make(plan.populations.length(), 0)
  let reroutes = Array::make(plan.populations.length(), 0)
  let mut peak_stalled = 0
  let mut peak_risk = 0.0
  let mut executed_steps = 0
  let mut last_queues = Array::make(plan.passages.length(), 0)
  for step = 0; step < config.maximum_steps; step = step + 1 {
    advance_hazard(plan, state, config.hazard)
    update_hazard_blocks(plan, state, config.hazard)
    let queues = build_queues(plan, state, cohorts, step, config)
    last_queues = queues
    update_peak_queues(queues, peak_queues)
    let capacity = passage_step_capacity(plan, plan.timestep_seconds)
    advance_cohorts(
      plan, state, cohorts, queues, capacity, step, config, first_exit, last_exit,
      evacuated, reroutes,
    )
    executed_steps = step + 1
    let observation = observe_risk(state)
    if observation.maximum_risk > peak_risk {
      peak_risk = observation.maximum_risk
    }
    let snapshot_every = int_max(1, config.snapshot_interval)
    if step % snapshot_every == 0 || cohort_people(cohorts) == 0 {
      let snapshot = record_snapshot(plan, state, cohorts, queues, total_people)
      if snapshot.stalled > peak_stalled {
        peak_stalled = snapshot.stalled
      }
      snapshots.push(snapshot)
    }
    if config.stop_when_evacuated && cohort_people(cohorts) == 0 {
      break
    }
  }
  if snapshots.length() == 0 ||
    snapshots[snapshots.length() - 1].step != state.step {
    let final_snapshot = record_snapshot(
      plan, state, cohorts, last_queues, total_people,
    )
    if final_snapshot.stalled > peak_stalled {
      peak_stalled = final_snapshot.stalled
    }
    snapshots.push(final_snapshot)
  }
  let outcomes = build_group_outcomes(
    plan, cohorts, first_exit, last_exit, evacuated, reroutes,
  )
  let remaining = cohort_people(cohorts)
  let evacuated_people = total_people - remaining
  {
    plan_name: plan.name,
    completed: remaining == 0,
    steps: executed_steps,
    total_people,
    evacuated_people,
    remaining_people: remaining,
    peak_stalled_people: peak_stalled,
    peak_risk,
    mean_evacuation_step: mean_evacuation_step(outcomes),
    snapshots,
    group_outcomes: outcomes,
    passage_peak_queues: peak_queues,
    event_log: state.event_log,
  }
}

///|
/// Compare two results with a stable lower-is-better score.
pub fn result_score(result : SimulationResult) -> Double {
  let incomplete_penalty = result.remaining_people.to_double() * 10000.0
  let time_penalty = result.steps.to_double()
  let stall_penalty = result.peak_stalled_people.to_double() * 5.0
  let risk_penalty = result.peak_risk * 1000.0
  incomplete_penalty + time_penalty + stall_penalty + risk_penalty
}

///|
/// Return the name of the better scenario, or "tie".
pub fn compare_results(
  left_name : String,
  left : SimulationResult,
  right_name : String,
  right : SimulationResult,
) -> String {
  let left_score = result_score(left)
  let right_score = result_score(right)
  if left_score < right_score {
    left_name
  } else if right_score < left_score {
    right_name
  } else {
    "tie"
  }
}

///|
/// Count total people at risk at or above a threshold in one snapshot.
pub fn snapshot_people_in_risky_places(
  snapshot : SimulationSnapshot,
  risks : Array[Double],
  threshold : Double,
) -> Int {
  let count = int_min(snapshot.place_occupancy.length(), risks.length())
  let mut total = 0
  for i = 0; i < count; i = i + 1 {
    if risks[i] >= threshold {
      total = total + snapshot.place_occupancy[i]
    }
  }
  total
}