///|
/// Validate a supplied plan and trace before rendering Markdown.
pub fn FlowGraph::to_markdown_checked(
  self : FlowGraph,
  plan : ExecutionPlan,
  trace : Trace,
) -> Result[String, SnapshotError] {
  match self.validate_snapshot(plan, trace) {
    Err(err) => Err(err)
    Ok(_) => Ok(self.to_markdown(plan, trace))
  }
}

///|
/// Build a fresh plan, validate trace references, and render Markdown.
pub fn FlowGraph::snapshot_markdown(
  self : FlowGraph,
  trace : Trace,
) -> Result[String, SnapshotError] {
  match self.plan() {
    Err(err) => Err(SnapshotGraphError(err))
    Ok(plan) => self.to_markdown_checked(plan, trace)
  }
}

///|
/// Render a graph, plan, and trace as a Markdown execution report.
///
/// This compatibility API trusts the supplied plan. New code should prefer
/// `to_markdown_checked` or `snapshot_markdown`.
pub fn FlowGraph::to_markdown(
  self : FlowGraph,
  plan : ExecutionPlan,
  trace : Trace,
) -> String {
  let out = StringBuilder()
  let order = plan.order()
  let batches = plan.batches()
  let tasks = self.tasks()
  let events = trace.events()
  out <+ "# MoonFlowGraph Report\n\n"
  out <+ "## Execution Order\n\n"
  for i = 0; i < order.length(); i = i + 1 {
    out <+ "\{i + 1}. \{order[i].value()}\n"
  }
  out <+ "\n## Parallel Batches\n\n"
  for i = 0; i < batches.length(); i = i + 1 {
    out <+ "- Batch \{i + 1}: "
    let ids = join_ids(batches[i], ", ")
    out.write_string(ids)
    out <+ "\n"
  }
  out <+ "\n## Tasks\n\n"
  for task in tasks {
    out <+ "### \{task.id().value()}: \{task.title()}\n\n"
    if task.description() != "" {
      out <+ "\{task.description()}\n\n"
    }
    out <+ "- Status: \{task.status().label()}\n"
    let inputs = join_strings(task.inputs(), ", ")
    let outputs = join_strings(task.outputs(), ", ")
    let tags = join_strings(task.tags(), ", ")
    out <+ "- Inputs: \{inputs}\n"
    out <+ "- Outputs: \{outputs}\n"
    out <+ "- Tags: \{tags}\n\n"
  }
  out <+ "## Trace\n\n"
  for event in events {
    out <+ "- `\{event.timestamp()}` \{event.task_id().value()} "
    out <+ "\{event.event_type().label()}: \{event.message()}\n"
  }
  out.to_string()
}

///|
/// Validate a supplied plan and trace before rendering JSON.
pub fn FlowGraph::to_json_checked(
  self : FlowGraph,
  plan : ExecutionPlan,
  trace : Trace,
) -> Result[String, SnapshotError] {
  match self.validate_snapshot(plan, trace) {
    Err(err) => Err(err)
    Ok(_) => Ok(self.render_json(plan, trace, versioned=true))
  }
}

///|
/// Build a fresh plan, validate trace references, and render JSON.
pub fn FlowGraph::snapshot_json(
  self : FlowGraph,
  trace : Trace,
) -> Result[String, SnapshotError] {
  match self.plan() {
    Err(err) => Err(SnapshotGraphError(err))
    Ok(plan) => self.to_json_checked(plan, trace)
  }
}

///|
/// Render a compact JSON string for tooling or snapshot artifacts.
///
/// This compatibility API trusts the supplied plan. New code should prefer
/// `to_json_checked` or `snapshot_json`.
pub fn FlowGraph::to_json(
  self : FlowGraph,
  plan : ExecutionPlan,
  trace : Trace,
) -> String {
  self.render_json(plan, trace, versioned=false)
}

///|
fn FlowGraph::render_json(
  self : FlowGraph,
  plan : ExecutionPlan,
  trace : Trace,
  versioned~ : Bool,
) -> String {
  let out = StringBuilder()
  let order = plan.order()
  let batches = plan.batches()
  let tasks = self.tasks()
  let dependencies = self.dependencies()
  let events = trace.events()
  out <+ "{\n"
  if versioned {
    out <+ "  \"schema_version\": 1,\n"
  }
  out <+ "  \"order\": ["
  for i = 0; i < order.length(); i = i + 1 {
    if i > 0 {
      out <+ ", "
    }
    let id = escape_json(order[i].value())
    out <+ "\"\{id}\""
  }
  out <+ "],\n"
  out <+ "  \"batches\": ["
  for i = 0; i < batches.length(); i = i + 1 {
    if i > 0 {
      out <+ ", "
    }
    out <+ "["
    for j = 0; j < batches[i].length(); j = j + 1 {
      if j > 0 {
        out <+ ", "
      }
      let id = escape_json(batches[i][j].value())
      out <+ "\"\{id}\""
    }
    out <+ "]"
  }
  out <+ "],\n"
  out <+ "  \"tasks\": ["
  for i = 0; i < tasks.length(); i = i + 1 {
    if i > 0 {
      out <+ ", "
    }
    let task = tasks[i]
    out <+ "{"
    let id = escape_json(task.id().value())
    let title = escape_json(task.title())
    let description = escape_json(task.description())
    let status = escape_json(task.status().label())
    let status_kind = escape_json(task.status().kind())
    out <+ "\"id\":\"\{id}\","
    out <+ "\"title\":\"\{title}\","
    out <+ "\"description\":\"\{description}\","
    out <+ "\"status\":\"\{status}\","
    if versioned {
      out <+ "\"status_kind\":\"\{status_kind}\","
      out <+ "\"status_reason\":"
      match task.status().reason() {
        Some(reason) => {
          let escaped_reason = escape_json(reason)
          out <+ "\"\{escaped_reason}\""
        }
        None => out <+ "null"
      }
      out <+ ","
    }
    out <+ "\"inputs\":"
    write_json_strings(out, task.inputs())
    out <+ ",\"outputs\":"
    write_json_strings(out, task.outputs())
    out <+ ",\"tags\":"
    write_json_strings(out, task.tags())
    out <+ "}"
  }
  out <+ "],\n"
  out <+ "  \"dependencies\": ["
  for i = 0; i < dependencies.length(); i = i + 1 {
    if i > 0 {
      out <+ ", "
    }
    let dep = dependencies[i]
    let before = escape_json(dep.before().value())
    let after = escape_json(dep.after().value())
    out <+ "{\"before\":\"\{before}\",\"after\":\"\{after}\"}"
  }
  out <+ "],\n"
  out <+ "  \"trace\": ["
  for i = 0; i < events.length(); i = i + 1 {
    if i > 0 {
      out <+ ", "
    }
    let event = events[i]
    out <+ "{"
    let task_id = escape_json(event.task_id().value())
    let event_type = escape_json(event.event_type().label())
    let timestamp = escape_json(event.timestamp())
    let message = escape_json(event.message())
    out <+ "\"task_id\":\"\{task_id}\","
    out <+ "\"event_type\":\"\{event_type}\","
    out <+ "\"timestamp\":\"\{timestamp}\","
    out <+ "\"message\":\"\{message}\""
    out <+ "}"
  }
  out <+ "]\n"
  out <+ "}\n"
  out.to_string()
}

///|
/// Render a validated task graph as a Mermaid flowchart.
pub fn FlowGraph::to_mermaid(self : FlowGraph) -> Result[String, GraphError] {
  match self.validate() {
    Err(err) => Err(err)
    Ok(_) => {
      let tasks = self.tasks()
      let out = StringBuilder()
      out <+ "flowchart TD\n"
      for i = 0; i < tasks.length(); i = i + 1 {
        let label = escape_mermaid(
          "\{tasks[i].id().value()}: \{tasks[i].title()}",
        )
        out <+ "  task_\{i}[\"\{label}\"]\n"
      }
      for dep in self.dependencies() {
        let before = task_position(tasks, dep.before())
        let after = task_position(tasks, dep.after())
        out <+ "  task_\{before} --> task_\{after}\n"
      }
      Ok(out.to_string())
    }
  }
}

///|
fn write_json_strings(out : StringBuilder, values : Array[String]) -> Unit {
  out <+ "["
  for i = 0; i < values.length(); i = i + 1 {
    if i > 0 {
      out <+ ", "
    }
    let value = escape_json(values[i])
    out <+ "\"\{value}\""
  }
  out <+ "]"
}

///|
fn join_ids(ids : Array[TaskId], sep : String) -> String {
  let out = StringBuilder()
  for i = 0; i < ids.length(); i = i + 1 {
    if i > 0 {
      out.write_string(sep)
    }
    out.write_string(ids[i].value())
  }
  out.to_string()
}

///|
fn join_strings(items : Array[String], sep : String) -> String {
  if items.is_empty() {
    return "-"
  }
  let out = StringBuilder()
  for i = 0; i < items.length(); i = i + 1 {
    if i > 0 {
      out.write_string(sep)
    }
    out.write_string(items[i])
  }
  out.to_string()
}

///|
fn escape_json(s : String) -> String {
  let out = StringBuilder()
  for ch in s {
    match ch {
      '"' => out <+ "\\\""
      '\\' => out <+ "\\\\"
      '\n' => out <+ "\\n"
      '\r' => out <+ "\\r"
      '\t' => out <+ "\\t"
      _ => {
        let code = ch.to_int()
        if code < 0x20 {
          out <+ "\\u00"
          out.write_string(hex_digit(code / 16))
          out.write_string(hex_digit(code % 16))
        } else {
          out <+ "\{ch}"
        }
      }
    }
  }
  out.to_string()
}

///|
fn hex_digit(value : Int) -> String {
  match value {
    0 => "0"
    1 => "1"
    2 => "2"
    3 => "3"
    4 => "4"
    5 => "5"
    6 => "6"
    7 => "7"
    8 => "8"
    9 => "9"
    10 => "a"
    11 => "b"
    12 => "c"
    13 => "d"
    14 => "e"
    15 => "f"
    _ => abort("hex digit out of range")
  }
}

///|
fn task_position(tasks : Array[TaskNode], id : TaskId) -> Int {
  for i = 0; i < tasks.length(); i = i + 1 {
    if tasks[i].id() == id {
      return i
    }
  }
  -1
}

///|
fn escape_mermaid(s : String) -> String {
  let out = StringBuilder()
  for ch in s {
    match ch {
      '&' => out <+ "&"
      '"' => out <+ """
      '\n' => out <+ " "
      '\r' => continue
      _ => out <+ "\{ch}"
    }
  }
  out.to_string()
}