///|
/// Tunable parameters for the graph-based hazard model.
pub struct HazardConfig {
  spread_rate : Double
  source_growth : Double
  passive_decay : Double
  door_transfer : Double
  hallway_transfer : Double
  stair_transfer : Double
  ramp_transfer : Double
  outdoor_transfer : Double
  floor_penalty : Double
  block_threshold : Double
} derive(Eq, Debug)

///|
/// Mutable scenario state at a specific simulation step.
pub struct ScenarioState {
  mut step : Int
  risks : Array[Double]
  active_sources : Array[Double]
  passage_enabled : Array[Bool]
  event_log : Array[String]
} derive(Eq, Debug)

///|
/// Risk classification for reporting and routing policy.
pub(all) enum RiskBand {
  Clear
  Caution
  Dangerous
  Critical
} derive(Eq, Debug)

///|
/// Aggregate risk observation for one time step.
pub struct RiskObservation {
  step : Int
  clear_places : Int
  caution_places : Int
  dangerous_places : Int
  critical_places : Int
  maximum_risk : Double
  mean_risk : Double
} derive(Eq, Debug)

///|
/// Conservative defaults for a teaching simulation.
pub fn HazardConfig::default() -> HazardConfig {
  {
    spread_rate: 0.12,
    source_growth: 0.08,
    passive_decay: 0.005,
    door_transfer: 0.85,
    hallway_transfer: 1.0,
    stair_transfer: 1.15,
    ramp_transfer: 0.95,
    outdoor_transfer: 0.35,
    floor_penalty: 0.8,
    block_threshold: 0.9,
  }
}

///|
/// A slower spread profile useful for drills.
pub fn HazardConfig::slow() -> HazardConfig {
  {
    spread_rate: 0.06,
    source_growth: 0.04,
    passive_decay: 0.008,
    door_transfer: 0.75,
    hallway_transfer: 0.9,
    stair_transfer: 1.0,
    ramp_transfer: 0.85,
    outdoor_transfer: 0.25,
    floor_penalty: 0.7,
    block_threshold: 0.92,
  }
}

///|
/// A fast spread stress-test profile.
pub fn HazardConfig::aggressive() -> HazardConfig {
  {
    spread_rate: 0.2,
    source_growth: 0.14,
    passive_decay: 0.002,
    door_transfer: 0.95,
    hallway_transfer: 1.1,
    stair_transfer: 1.3,
    ramp_transfer: 1.0,
    outdoor_transfer: 0.45,
    floor_penalty: 0.9,
    block_threshold: 0.82,
  }
}

///|
fn clamp_unit(value : Double) -> Double {
  if value < 0.0 {
    0.0
  } else if value > 1.0 {
    1.0
  } else {
    value
  }
}

///|
fn passage_transfer(config : HazardConfig, kind : PassageKind) -> Double {
  match kind {
    Door => config.door_transfer
    Hallway => config.hallway_transfer
    Stairway => config.stair_transfer
    Ramp => config.ramp_transfer
    Outdoor => config.outdoor_transfer
  }
}

///|
/// Initialize scenario state from plan defaults.
pub fn ScenarioState::from_plan(plan : EvacuationPlan) -> ScenarioState {
  let risks : Array[Double] = []
  let sources : Array[Double] = []
  let enabled : Array[Bool] = []
  for place in plan.places {
    risks.push(clamp_unit(place.initial_risk))
    sources.push(0.0)
  }
  for passage in plan.passages {
    enabled.push(passage.enabled)
  }
  {
    step: 0,
    risks,
    active_sources: sources,
    passage_enabled: enabled,
    event_log: [],
  }
}

///|
/// Copy scenario state so scenario branches are independent.
pub fn ScenarioState::copy(self : ScenarioState) -> ScenarioState {
  let risks : Array[Double] = []
  let sources : Array[Double] = []
  let enabled : Array[Bool] = []
  let log : Array[String] = []
  for value in self.risks {
    risks.push(value)
  }
  for value in self.active_sources {
    sources.push(value)
  }
  for value in self.passage_enabled {
    enabled.push(value)
  }
  for value in self.event_log {
    log.push(value)
  }
  {
    step: self.step,
    risks,
    active_sources: sources,
    passage_enabled: enabled,
    event_log: log,
  }
}

///|
/// Convert numeric risk to a stable reporting band.
pub fn risk_band(value : Double) -> RiskBand {
  if value < 0.2 {
    Clear
  } else if value < 0.5 {
    Caution
  } else if value < 0.8 {
    Dangerous
  } else {
    Critical
  }
}

///|
/// Stable code for a risk band.
pub fn RiskBand::code(self : RiskBand) -> String {
  match self {
    Clear => "clear"
    Caution => "caution"
    Dangerous => "dangerous"
    Critical => "critical"
  }
}

///|
/// Display color used by the SVG renderer.
pub fn RiskBand::color(self : RiskBand) -> String {
  match self {
    Clear => "#2e7d32"
    Caution => "#f9a825"
    Dangerous => "#ef6c00"
    Critical => "#c62828"
  }
}

///|
fn event_due(event : ScenarioEvent, step : Int) -> Bool {
  match event {
    ClosePassage(_, at) => at == step
    OpenPassage(_, at) => at == step
    Ignite(_, at, _) => at == step
    ClearRisk(_, at) => at == step
  }
}

///|
/// Apply all events due at the state's current step.
pub fn apply_due_events(plan : EvacuationPlan, state : ScenarioState) -> Unit {
  for event in plan.events {
    if event_due(event, state.step) {
      match event {
        ClosePassage(id, _) => {
          let index = plan.passage_index(id)
          if index >= 0 && index < state.passage_enabled.length() {
            state.passage_enabled[index] = false
            state.event_log.push("step \{state.step}: closed passage \{id}")
          } else {
            state.event_log.push(
              "step \{state.step}: ignored unknown passage \{id}",
            )
          }
        }
        OpenPassage(id, _) => {
          let index = plan.passage_index(id)
          if index >= 0 && index < state.passage_enabled.length() {
            state.passage_enabled[index] = true
            state.event_log.push("step \{state.step}: opened passage \{id}")
          } else {
            state.event_log.push(
              "step \{state.step}: ignored unknown passage \{id}",
            )
          }
        }
        Ignite(id, _, intensity) => {
          let index = plan.place_index(id)
          if index >= 0 && index < state.active_sources.length() {
            state.active_sources[index] = clamp_unit(intensity)
            if state.risks[index] < intensity {
              state.risks[index] = clamp_unit(intensity)
            }
            state.event_log.push(
              "step \{state.step}: ignited \{id} at \{intensity}",
            )
          } else {
            state.event_log.push(
              "step \{state.step}: ignored unknown place \{id}",
            )
          }
        }
        ClearRisk(id, _) => {
          let index = plan.place_index(id)
          if index >= 0 && index < state.risks.length() {
            state.risks[index] = 0.0
            state.active_sources[index] = 0.0
            state.event_log.push("step \{state.step}: cleared risk at \{id}")
          } else {
            state.event_log.push(
              "step \{state.step}: ignored unknown place \{id}",
            )
          }
        }
      }
    }
  }
}

///|
fn transfer_amount(
  plan : EvacuationPlan,
  state : ScenarioState,
  config : HazardConfig,
  passage_index : Int,
  from_index : Int,
  to_index : Int,
) -> Double {
  if passage_index < 0 || passage_index >= plan.passages.length() {
    return 0.0
  }
  if passage_index >= state.passage_enabled.length() ||
    !state.passage_enabled[passage_index] {
    return 0.0
  }
  let passage = plan.passages[passage_index]
  let source = state.risks[from_index]
  let destination = state.risks[to_index]
  if source <= destination {
    return 0.0
  }
  let floor_difference = if plan.places[from_index].floor ==
    plan.places[to_index].floor {
    1.0
  } else {
    config.floor_penalty
  }
  let distance_factor = 1.0 / (1.0 + passage.length_m / 20.0)
  let width_factor = if passage.width_m > 2.0 {
    1.2
  } else {
    0.6 + passage.width_m * 0.3
  }
  (source - destination) *
  config.spread_rate *
  passage_transfer(config, passage.kind) *
  floor_difference *
  distance_factor *
  width_factor
}

///|
/// Advance graph risk by one deterministic step.
pub fn advance_hazard(
  plan : EvacuationPlan,
  state : ScenarioState,
  config : HazardConfig,
) -> Unit {
  apply_due_events(plan, state)
  let delta = Array::make(plan.places.length(), 0.0)
  for i = 0; i < plan.places.length(); i = i + 1 {
    if i < state.active_sources.length() && state.active_sources[i] > 0.0 {
      delta[i] = delta[i] + state.active_sources[i] * config.source_growth
    } else {
      delta[i] = delta[i] - config.passive_decay
    }
  }
  for p = 0; p < plan.passages.length(); p = p + 1 {
    let passage = plan.passages[p]
    let from_index = plan.place_index(passage.from_id)
    let to_index = plan.place_index(passage.to_id)
    if from_index >= 0 && to_index >= 0 {
      let forward = transfer_amount(
        plan, state, config, p, from_index, to_index,
      )
      delta[to_index] = delta[to_index] + forward
      if passage.bidirectional {
        let backward = transfer_amount(
          plan, state, config, p, to_index, from_index,
        )
        delta[from_index] = delta[from_index] + backward
      }
    }
  }
  for i = 0; i < state.risks.length(); i = i + 1 {
    state.risks[i] = clamp_unit(state.risks[i] + delta[i])
  }
  state.step = state.step + 1
}

///|
/// Disable passages touching places above the configured block threshold.
pub fn update_hazard_blocks(
  plan : EvacuationPlan,
  state : ScenarioState,
  config : HazardConfig,
) -> Unit {
  for i = 0; i < plan.passages.length(); i = i + 1 {
    let passage = plan.passages[i]
    let from_index = plan.place_index(passage.from_id)
    let to_index = plan.place_index(passage.to_id)
    if from_index >= 0 && to_index >= 0 {
      if state.risks[from_index] >= config.block_threshold ||
        state.risks[to_index] >= config.block_threshold {
        state.passage_enabled[i] = false
      }
    }
  }
}

///|
/// Produce congestion input compatible with the routing engine.
pub fn normalized_passage_congestion(
  plan : EvacuationPlan,
  queued : Array[Int],
) -> Array[Double] {
  let result = Array::make(plan.passages.length(), 0.0)
  for i = 0; i < result.length(); i = i + 1 {
    let people = if i < queued.length() { queued[i] } else { 0 }
    let width = if plan.passages[i].width_m > 0.1 {
      plan.passages[i].width_m
    } else {
      0.1
    }
    result[i] = people.to_double() / (width * 10.0)
  }
  result
}

///|
/// Observe current risk distribution.
pub fn observe_risk(state : ScenarioState) -> RiskObservation {
  let mut clear_places = 0
  let mut caution_places = 0
  let mut dangerous_places = 0
  let mut critical_places = 0
  let mut maximum_risk = 0.0
  let mut sum = 0.0
  for risk in state.risks {
    if risk > maximum_risk {
      maximum_risk = risk
    }
    sum = sum + risk
    match risk_band(risk) {
      Clear => clear_places = clear_places + 1
      Caution => caution_places = caution_places + 1
      Dangerous => dangerous_places = dangerous_places + 1
      Critical => critical_places = critical_places + 1
    }
  }
  let mean = if state.risks.length() > 0 {
    sum / state.risks.length().to_double()
  } else {
    0.0
  }
  {
    step: state.step,
    clear_places,
    caution_places,
    dangerous_places,
    critical_places,
    maximum_risk,
    mean_risk: mean,
  }
}

///|
/// Return ids currently above a chosen risk threshold.
pub fn risky_place_ids(
  plan : EvacuationPlan,
  state : ScenarioState,
  threshold : Double,
) -> Array[String] {
  let result : Array[String] = []
  let count = if plan.places.length() < state.risks.length() {
    plan.places.length()
  } else {
    state.risks.length()
  }
  for i = 0; i < count; i = i + 1 {
    if state.risks[i] >= threshold {
      result.push(plan.places[i].id)
    }
  }
  result
}

///|
/// Run a hazard-only preview and collect observations.
pub fn hazard_preview(
  plan : EvacuationPlan,
  initial : ScenarioState,
  config : HazardConfig,
  steps : Int,
) -> Array[RiskObservation] {
  let state = initial.copy()
  let result : Array[RiskObservation] = [observe_risk(state)]
  let count = if steps < 0 { 0 } else { steps }
  for _step = 0; _step < count; _step = _step + 1 {
    advance_hazard(plan, state, config)
    update_hazard_blocks(plan, state, config)
    result.push(observe_risk(state))
  }
  result
}

///|
/// Integrate risk exposure over a series of risk samples.
pub fn accumulated_exposure(
  samples : Array[Double],
  timestep : Double,
) -> Double {
  if samples.length() == 0 || timestep <= 0.0 {
    return 0.0
  }
  let mut area = 0.0
  for i = 0; i < samples.length(); i = i + 1 {
    let weight = if i == 0 || i == samples.length() - 1 { 0.5 } else { 1.0 }
    area = area + samples[i] * weight * timestep
  }
  area
}