///|
/// Escape arbitrary text for JSON string values.
pub fn escape_json(text : String) -> String {
  text
  .replace_all(old="\\", new="\\\\")
  .replace_all(old="\"", new="\\\"")
  .replace_all(old="\n", new="\\n")
  .replace_all(old="\r", new="\\r")
  .replace_all(old="\t", new="\\t")
}

///|
/// Escape arbitrary text for XML and SVG text nodes.
pub fn escape_xml(text : String) -> String {
  text
  .replace_all(old="&", new="&")
  .replace_all(old="<", new="<")
  .replace_all(old=">", new=">")
  .replace_all(old="\"", new=""")
  .replace_all(old="'", new="'")
}

///|
fn bool_json(value : Bool) -> String {
  if value {
    "true"
  } else {
    "false"
  }
}

///|
fn json_string(value : String) -> String {
  "\"\{escape_json(value)}\""
}

///|
fn join_json_strings(values : Array[String]) -> String {
  let mut text = "["
  for i = 0; i < values.length(); i = i + 1 {
    if i > 0 {
      text = text + ","
    }
    text = text + json_string(values[i])
  }
  text + "]"
}

///|
fn join_json_ints(values : Array[Int]) -> String {
  let mut text = "["
  for i = 0; i < values.length(); i = i + 1 {
    if i > 0 {
      text = text + ","
    }
    text = text + values[i].to_string()
  }
  text + "]"
}

///|
fn severity_icon(severity : Severity) -> String {
  match severity {
    Info => "ℹ️"
    Warning => "⚠️"
    Error => "❌"
  }
}

///|
/// Render validation diagnostics as a compact Markdown document.
pub fn validation_markdown(
  plan : EvacuationPlan,
  diagnostics : Array[Diagnostic],
) -> String {
  let errors = diagnostic_count(diagnostics, Error)
  let warnings = diagnostic_count(diagnostics, Warning)
  let infos = diagnostic_count(diagnostics, Info)
  let mut text = "# MoonEvac validation report\n\n"
  text = text + "- Plan: **\{plan.name}**\n"
  text = text + "- Status: **\{if errors == 0 { "PASS" } else { "FAIL" }}**\n"
  text = text + "- Places: \{plan.places.length()}\n"
  text = text + "- Passages: \{plan.passages.length()}\n"
  text = text + "- Population: \{plan.population_count()}\n"
  text = text + "- Errors: \{errors}\n"
  text = text + "- Warnings: \{warnings}\n"
  text = text + "- Information: \{infos}\n\n"
  if diagnostics.length() == 0 {
    text = text + "No validation problems were found.\n"
    return text
  }
  text = text + "## Findings\n\n"
  for item in diagnostics {
    text = text + "### \{severity_icon(item.severity)} `\{item.code}`\n\n"
    text = text + "- Subject: `\{item.subject}`\n"
    text = text + "- Severity: \{item.severity.code()}\n"
    text = text + "- Finding: \{item.message}\n"
    text = text + "- Suggested action: \{item.hint}\n\n"
  }
  text
}

///|
/// Render a full simulation result as Markdown.
pub fn simulation_markdown(result : SimulationResult) -> String {
  let mut text = "# MoonEvac simulation report\n\n"
  text = text + "## Summary\n\n"
  text = text + "| Metric | Value |\n"
  text = text + "|---|---:|\n"
  text = text + "| Plan | \{result.plan_name} |\n"
  text = text + "| Completed | \{result.completed} |\n"
  text = text + "| Simulated steps | \{result.steps} |\n"
  text = text + "| Initial population | \{result.total_people} |\n"
  text = text + "| Evacuated | \{result.evacuated_people} |\n"
  text = text + "| Remaining | \{result.remaining_people} |\n"
  text = text + "| Peak stalled population | \{result.peak_stalled_people} |\n"
  text = text + "| Peak risk | \{result.peak_risk} |\n"
  text = text + "| Mean evacuation step | \{result.mean_evacuation_step} |\n\n"
  text = text + "## Population groups\n\n"
  text = text +
    "| Group | Initial | Evacuated | Remaining | First exit | Last exit | Reroutes | Exposure |\n"
  text = text + "|---|---:|---:|---:|---:|---:|---:|---:|\n"
  for outcome in result.group_outcomes {
    text = text + "| \{outcome.group_id} | \{outcome.initial_count} | "
    text = text + "\{outcome.evacuated_count} | \{outcome.remaining_count} | "
    text = text + "\{outcome.first_exit_step} | \{outcome.last_exit_step} | "
    text = text + "\{outcome.reroutes} | \{outcome.accumulated_exposure} |\n"
  }
  text = text + "\n## Timeline\n\n"
  text = text +
    "| Step | Remaining | Evacuated | Stalled | Maximum risk | Mean risk |\n"
  text = text + "|---:|---:|---:|---:|---:|---:|\n"
  for snapshot in result.snapshots {
    text = text + "| \{snapshot.step} | \{snapshot.remaining} | "
    text = text + "\{snapshot.evacuated} | \{snapshot.stalled} | "
    text = text + "\{snapshot.maximum_risk} | \{snapshot.mean_risk} |\n"
  }
  if result.event_log.length() > 0 {
    text = text + "\n## Scenario events\n\n"
    for event in result.event_log {
      text = text + "- \{event}\n"
    }
  }
  text
}

///|
fn diagnostic_json(item : Diagnostic) -> String {
  "{" +
  "\"code\":" +
  json_string(item.code) +
  "," +
  "\"severity\":" +
  json_string(item.severity.code()) +
  "," +
  "\"subject\":" +
  json_string(item.subject) +
  "," +
  "\"message\":" +
  json_string(item.message) +
  "," +
  "\"hint\":" +
  json_string(item.hint) +
  "}"
}

///|
/// Render diagnostics as stable JSON without external dependencies.
pub fn validation_json(
  plan : EvacuationPlan,
  diagnostics : Array[Diagnostic],
) -> String {
  let mut items = "["
  for i = 0; i < diagnostics.length(); i = i + 1 {
    if i > 0 {
      items = items + ","
    }
    items = items + diagnostic_json(diagnostics[i])
  }
  items = items + "]"
  "{" +
  "\"schema\":\"moonevac.validation.v1\"," +
  "\"plan\":" +
  json_string(plan.name) +
  "," +
  "\"valid\":" +
  bool_json(validation_ok(diagnostics)) +
  "," +
  "\"place_count\":\{plan.places.length()}," +
  "\"passage_count\":\{plan.passages.length()}," +
  "\"population\":\{plan.population_count()}," +
  "\"diagnostics\":" +
  items +
  "}"
}

///|
fn group_outcome_json(outcome : GroupOutcome) -> String {
  "{" +
  "\"group_id\":" +
  json_string(outcome.group_id) +
  "," +
  "\"initial\":\{outcome.initial_count}," +
  "\"evacuated\":\{outcome.evacuated_count}," +
  "\"remaining\":\{outcome.remaining_count}," +
  "\"first_exit_step\":\{outcome.first_exit_step}," +
  "\"last_exit_step\":\{outcome.last_exit_step}," +
  "\"exposure\":\{outcome.accumulated_exposure}," +
  "\"reroutes\":\{outcome.reroutes}" +
  "}"
}

///|
fn snapshot_json(snapshot : SimulationSnapshot) -> String {
  "{" +
  "\"step\":\{snapshot.step}," +
  "\"remaining\":\{snapshot.remaining}," +
  "\"evacuated\":\{snapshot.evacuated}," +
  "\"stalled\":\{snapshot.stalled}," +
  "\"maximum_risk\":\{snapshot.maximum_risk}," +
  "\"mean_risk\":\{snapshot.mean_risk}," +
  "\"passage_queues\":" +
  join_json_ints(snapshot.passage_queues) +
  "," +
  "\"place_occupancy\":" +
  join_json_ints(snapshot.place_occupancy) +
  "}"
}

///|
/// Render a simulation result as stable JSON.
pub fn simulation_json(result : SimulationResult) -> String {
  let mut groups = "["
  for i = 0; i < result.group_outcomes.length(); i = i + 1 {
    if i > 0 {
      groups = groups + ","
    }
    groups = groups + group_outcome_json(result.group_outcomes[i])
  }
  groups = groups + "]"
  let mut snapshots = "["
  for i = 0; i < result.snapshots.length(); i = i + 1 {
    if i > 0 {
      snapshots = snapshots + ","
    }
    snapshots = snapshots + snapshot_json(result.snapshots[i])
  }
  snapshots = snapshots + "]"
  "{" +
  "\"schema\":\"moonevac.simulation.v1\"," +
  "\"plan\":" +
  json_string(result.plan_name) +
  "," +
  "\"completed\":" +
  bool_json(result.completed) +
  "," +
  "\"steps\":\{result.steps}," +
  "\"total_people\":\{result.total_people}," +
  "\"evacuated_people\":\{result.evacuated_people}," +
  "\"remaining_people\":\{result.remaining_people}," +
  "\"peak_stalled_people\":\{result.peak_stalled_people}," +
  "\"peak_risk\":\{result.peak_risk}," +
  "\"mean_evacuation_step\":\{result.mean_evacuation_step}," +
  "\"groups\":" +
  groups +
  "," +
  "\"snapshots\":" +
  snapshots +
  "," +
  "\"passage_peak_queues\":" +
  join_json_ints(result.passage_peak_queues) +
  "," +
  "\"events\":" +
  join_json_strings(result.event_log) +
  "}"
}

///|
/// Render timeline data as CSV for spreadsheet analysis.
pub fn simulation_timeline_csv(result : SimulationResult) -> String {
  let mut text = "step,remaining,evacuated,stalled,maximum_risk,mean_risk\n"
  for snapshot in result.snapshots {
    text = text + "\{snapshot.step},\{snapshot.remaining},"
    text = text + "\{snapshot.evacuated},\{snapshot.stalled},"
    text = text + "\{snapshot.maximum_risk},\{snapshot.mean_risk}\n"
  }
  text
}

///|
/// Render group outcomes as CSV.
pub fn simulation_groups_csv(result : SimulationResult) -> String {
  let mut text = "group,initial,evacuated,remaining,first_exit,last_exit,reroutes,exposure\n"
  for outcome in result.group_outcomes {
    text = text + "\{outcome.group_id},\{outcome.initial_count},"
    text = text + "\{outcome.evacuated_count},\{outcome.remaining_count},"
    text = text + "\{outcome.first_exit_step},\{outcome.last_exit_step},"
    text = text + "\{outcome.reroutes},\{outcome.accumulated_exposure}\n"
  }
  text
}

///|
fn svg_point(place : Place, floor_offset : Double) -> Point {
  { x: place.x + floor_offset, y: place.y }
}

///|
fn risk_color(risks : Array[Double], index : Int) -> String {
  if index < 0 || index >= risks.length() {
    "#607d8b"
  } else {
    risk_band(risks[index]).color()
  }
}

///|
fn place_shape(place : Place) -> String {
  match place.kind {
    Room => "rect"
    Corridor => "circle"
    Stair => "diamond"
    Assembly => "hexagon"
    Exit => "exit"
  }
}

///|
fn svg_place_node(
  place : Place,
  risk : String,
  floor_offset : Double,
) -> String {
  let point = svg_point(place, floor_offset)
  let label = escape_xml(place.label)
  let id = escape_xml(place.id)
  let shape = place_shape(place)
  let mut body = ""
  match place.kind {
    Room =>
      body = body +
        ""
    Corridor =>
      body = body +
        ""
    Stair =>
      body = body +
        ""
    Assembly =>
      body = body +
        ""
    Exit =>
      body = body +
        ""
  }
  body = body +
    "\{label}"
  body + ""
}

///|
fn svg_passage(
  plan : EvacuationPlan,
  passage : Passage,
  floor_gap : Double,
) -> String {
  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 {
    return ""
  }
  let from = plan.places[from_index]
  let to = plan.places[to_index]
  let a = svg_point(from, (from.floor - 1).to_double() * floor_gap)
  let b = svg_point(to, (to.floor - 1).to_double() * floor_gap)
  let dash = if passage.kind == Stairway || passage.kind == Ramp {
    " stroke-dasharray=\"7 5\""
  } else {
    ""
  }
  let width = if passage.width_m < 1.0 { 2.0 } else { passage.width_m * 2.0 }
  ""
}

///|
/// Render the plan and an optional risk state as a standalone SVG document.
pub fn plan_svg(plan : EvacuationPlan, risks : Array[Double]) -> String {
  let floor_gap = 540.0
  let mut maximum_floor = 1
  for place in plan.places {
    if place.floor > maximum_floor {
      maximum_floor = place.floor
    }
  }
  let width = maximum_floor.to_double() * floor_gap
  let mut text = ""
  text = text +
    ""
  text = text + ""
  for floor = 1; floor <= maximum_floor; floor = floor + 1 {
    let x = (floor - 1).to_double() * floor_gap
    text = text +
      ""
    text = text +
      "Floor \{floor}"
  }
  for passage in plan.passages {
    text = text + svg_passage(plan, passage, floor_gap)
  }
  for i = 0; i < plan.places.length(); i = i + 1 {
    let place = plan.places[i]
    text = text +
      svg_place_node(
        place,
        risk_color(risks, i),
        (place.floor - 1).to_double() * floor_gap,
      )
  }
  text + ""
}

///|
fn chart_x(index : Int, count : Int, width : Double) -> Double {
  if count <= 1 {
    60.0
  } else {
    60.0 + index.to_double() * (width - 100.0) / (count - 1).to_double()
  }
}

///|
fn chart_y(value : Double, maximum : Double, height : Double) -> Double {
  if maximum <= 0.0 {
    height - 40.0
  } else {
    height - 40.0 - value / maximum * (height - 90.0)
  }
}

///|
/// Render remaining and evacuated population as an SVG timeline.
pub fn timeline_svg(result : SimulationResult) -> String {
  let width = 900.0
  let height = 360.0
  let maximum = if result.total_people > 0 {
    result.total_people.to_double()
  } else {
    1.0
  }
  let mut remaining_points = ""
  let mut evacuated_points = ""
  for i = 0; i < result.snapshots.length(); i = i + 1 {
    let snapshot = result.snapshots[i]
    let x = chart_x(i, result.snapshots.length(), width)
    let remaining_y = chart_y(snapshot.remaining.to_double(), maximum, height)
    let evacuated_y = chart_y(snapshot.evacuated.to_double(), maximum, height)
    if i > 0 {
      remaining_points = remaining_points + " "
      evacuated_points = evacuated_points + " "
    }
    remaining_points = remaining_points + "\{x},\{remaining_y}"
    evacuated_points = evacuated_points + "\{x},\{evacuated_y}"
  }
  let mut text = ""
  text = text + ""
  text = text +
    ""
  text = text +
    ""
  text = text +
    ""
  text = text +
    ""
  text = text +
    "Remaining"
  text = text +
    "Evacuated"
  text + ""
}