///|
/// Render a human-readable Markdown report for the entire tracking store.
pub fn TrackingStore::to_markdown(self : TrackingStore) -> String {
  let out = StringBuilder()
  out <+ "# MoonTrack Experiment Report\n\n"
  if self.experiments.is_empty() {
    out <+ "No experiments recorded.\n"
    return out.to_string()
  }
  for exp in self.experiments {
    out <+ "## Experiment: \{exp.name()}\n\n"
    out <+ "- **ID**: `\{exp.id()}`\n"
    if exp.description() != "" {
      out <+ "- **Description**: \{exp.description()}\n"
    }
    out <+ "- **Runs**: \{exp.run_count()}\n"
    let tags = exp.tags()
    if tags.length() > 0 {
      out <+ "- **Tags**: "
      for i = 0; i < tags.length(); i = i + 1 {
        if i > 0 {
          out <+ ", "
        }
        out <+ "`\{tags[i]}`"
      }
      out <+ "\n"
    }
    out <+ "\n"
    // List runs for this experiment
    let runs = self.runs_for_experiment(exp.id())
    if runs.is_empty() {
      out <+ "_No runs yet._\n\n"
    } else {
      out <+ "### Runs\n\n"
      out <+
        "| Run ID | Status | Start | End | Params | Metrics | Artifacts |\n"
      out <+ "|--------|--------|-------|-----|--------|---------|----------|\n"
      for run in runs {
        let id = run.id()
        let status = run.status().label()
        let start = run.start_time()
        let end = if run.end_time() == "" { "-" } else { run.end_time() }
        let params = run.param_count()
        let metrics = run.metric_count()
        let artifacts = run.artifact_count()
        out <+
          "| `\{id}` | \{status} | \{start} | \{end} | \{params} | \{metrics} | \{artifacts} |\n"
      }
      out <+ "\n"
    }
  }
  out.to_string()
}

///|
/// Render a detailed Markdown report for a single run.
pub fn TrackingStore::run_markdown(
  self : TrackingStore,
  run_id : String,
) -> Result[String, TrackingError] {
  let run = match self.get_run(run_id) {
    Err(err) => return Err(err)
    Ok(r) => r
  }
  let out = StringBuilder()
  out <+ "## Run: `\{run.id()}`\n\n"
  out <+ "- **Experiment**: `\{run.experiment_id()}`\n"
  out <+ "- **Status**: \{run.status().label()}\n"
  out <+ "- **Start**: \{run.start_time()}\n"
  if run.end_time() != "" {
    out <+ "- **End**: \{run.end_time()}\n"
  }
  if run.error_message() != "" {
    out <+ "- **Error**: \{run.error_message()}\n"
  }
  let tags = run.tags()
  if tags.length() > 0 {
    out <+ "- **Tags**: "
    for i = 0; i < tags.length(); i = i + 1 {
      if i > 0 {
        out <+ ", "
      }
      out <+ "`\{tags[i]}`"
    }
    out <+ "\n"
  }
  // Parameters
  let params = run.parameters()
  if params.length() > 0 {
    out <+ "\n### Parameters\n\n"
    out <+ "| Key | Value | Type |\n"
    out <+ "|-----|-------|------|\n"
    for p in params {
      out <+ "| `\{p.key()}` | \{p.value()} | \{p.type_().label()} |\n"
    }
  }
  // Metrics
  let metrics = run.metrics()
  if metrics.length() > 0 {
    out <+ "\n### Metrics\n\n"
    out <+ "| Key | Value | Step | Direction |\n"
    out <+ "|-----|-------|------|----------|\n"
    for m in metrics {
      out <+
        "| `\{m.key()}` | \{m.value()} | \{m.step()} | \{m.direction().label()} |\n"
    }
  }
  // Artifacts
  let artifacts = run.artifacts()
  if artifacts.length() > 0 {
    out <+ "\n### Artifacts\n\n"
    out <+ "| Name | Path | Size |\n"
    out <+ "|------|------|------|\n"
    for a in artifacts {
      out <+ "| `\{a.name()}` | \{a.path()} | \{a.size()} |\n"
    }
  }
  // Reproducibility
  match run.reproducibility() {
    Some(info) => {
      out <+ "\n### Reproducibility\n\n"
      out <+ "- **Code version**: `\{info.code_version()}`\n"
      out <+ "- **Command**: `\{info.command()}`\n"
      match info.random_seed() {
        Some(seed) => out <+ "- **Random seed**: \{seed}\n"
        None => ()
      }
    }
    None => ()
  }
  Ok(out.to_string())
}

///|
/// Render the entire tracking store as a JSON string.
pub fn TrackingStore::to_json(self : TrackingStore) -> String {
  let out = StringBuilder()
  out <+ "{\n"
  out <+ "  \"schema_version\": 1,\n"
  out <+ "  \"experiments\": ["
  if self.experiments.is_empty() {
    out <+ "],\n"
  } else {
    out <+ "\n"
    for i = 0; i < self.experiments.length(); i = i + 1 {
      if i > 0 {
        out <+ ",\n"
      }
      self.experiments[i].render_json(out)
    }
    out <+ "\n  ],\n"
  }
  out <+ "  \"runs\": ["
  if self.runs.is_empty() {
    out <+ "]\n"
  } else {
    out <+ "\n"
    for i = 0; i < self.runs.length(); i = i + 1 {
      if i > 0 {
        out <+ ",\n"
      }
      self.runs[i].render_json(out)
    }
    out <+ "\n  ]\n"
  }
  out <+ "}"
  out.to_string()
}

///|
/// Render an experiment as JSON.
fn Experiment::render_json(self : Experiment, out : StringBuilder) -> Unit {
  out <+ "  {\n"
  out <+ "    \"id\": \"\{escape_json(self.id())}\",\n"
  out <+ "    \"name\": \"\{escape_json(self.name())}\",\n"
  out <+ "    \"description\": \"\{escape_json(self.description())}\",\n"
  // Tags
  let tags = self.tags()
  out <+ "    \"tags\": ["
  for i = 0; i < tags.length(); i = i + 1 {
    if i > 0 {
      out <+ ", "
    }
    out <+ "\"\{escape_json(tags[i])}\""
  }
  out <+ "],\n"
  out <+ "    \"created_at\": \"\{escape_json(self.created_at())}\",\n"
  // Metadata
  let meta = self.metadata()
  let meta_keys : Array[String] = []
  meta.each(fn(k, _v) { meta_keys.push(k) })
  out <+ "    \"metadata\": {"
  for i = 0; i < meta_keys.length(); i = i + 1 {
    if i > 0 {
      out <+ ", "
    }
    let k = meta_keys[i]
    let v = match meta.get(k) {
      Some(v) => v
      None => ""
    }
    out <+ "\"\{escape_json(k)}\": \"\{escape_json(v)}\""
  }
  out <+ "},\n"
  // Run IDs
  let run_ids = self.run_ids()
  out <+ "    \"run_ids\": ["
  for i = 0; i < run_ids.length(); i = i + 1 {
    if i > 0 {
      out <+ ", "
    }
    out <+ "\"\{escape_json(run_ids[i])}\""
  }
  out <+ "]\n"
  out <+ "  }"
}

///|
/// Render a run as JSON.
fn Run::render_json(self : Run, out : StringBuilder) -> Unit {
  out <+ "  {\n"
  out <+ "    \"id\": \"\{escape_json(self.id())}\",\n"
  out <+ "    \"experiment_id\": \"\{escape_json(self.experiment_id())}\",\n"
  out <+ "    \"status\": \"\{self.status().label()}\",\n"
  out <+ "    \"start_time\": \"\{escape_json(self.start_time())}\",\n"
  out <+ "    \"end_time\": \"\{escape_json(self.end_time())}\",\n"
  out <+ "    \"error_message\": \"\{escape_json(self.error_message())}\",\n"
  // Tags
  let tags = self.tags()
  out <+ "    \"tags\": ["
  for i = 0; i < tags.length(); i = i + 1 {
    if i > 0 {
      out <+ ", "
    }
    out <+ "\"\{escape_json(tags[i])}\""
  }
  out <+ "],\n"
  // Parameters
  let params = self.parameters()
  out <+ "    \"parameters\": ["
  if params.length() == 0 {
    out <+ "],\n"
  } else {
    out <+ "\n"
    for i = 0; i < params.length(); i = i + 1 {
      if i > 0 {
        out <+ ",\n"
      }
      out <+ "      {"
      out <+ "\"key\": \"\{escape_json(params[i].key())}\", "
      out <+ "\"value\": \"\{escape_json(params[i].value())}\", "
      out <+ "\"type\": \"\{params[i].type_().kind()}\""
      out <+ "}"
    }
    out <+ "\n    ],\n"
  }
  // Metrics
  let metrics = self.metrics()
  out <+ "    \"metrics\": ["
  if metrics.length() == 0 {
    out <+ "],\n"
  } else {
    out <+ "\n"
    for i = 0; i < metrics.length(); i = i + 1 {
      if i > 0 {
        out <+ ",\n"
      }
      out <+ "      {"
      out <+ "\"key\": \"\{escape_json(metrics[i].key())}\", "
      out <+ "\"value\": \{metrics[i].value()}, "
      out <+ "\"step\": \{metrics[i].step()}, "
      out <+ "\"timestamp\": \"\{escape_json(metrics[i].timestamp())}\", "
      out <+ "\"direction\": \"\{metrics[i].direction().kind()}\""
      out <+ "}"
    }
    out <+ "\n    ],\n"
  }
  // Artifacts
  let artifacts = self.artifacts()
  out <+ "    \"artifacts\": ["
  if artifacts.length() == 0 {
    out <+ "],\n"
  } else {
    out <+ "\n"
    for i = 0; i < artifacts.length(); i = i + 1 {
      if i > 0 {
        out <+ ",\n"
      }
      out <+ "      {"
      out <+ "\"name\": \"\{escape_json(artifacts[i].name())}\", "
      out <+ "\"path\": \"\{escape_json(artifacts[i].path())}\", "
      out <+ "\"size\": \{artifacts[i].size()}"
      match artifacts[i].checksum() {
        Some(cs) => out <+ ", \"checksum\": \"\{escape_json(cs)}\""
        None => ()
      }
      out <+ "}"
    }
    out <+ "\n    ],\n"
  }
  // Reproducibility
  match self.reproducibility() {
    Some(info) => {
      out <+ "    \"reproducibility\": {\n"
      out <+
        "      \"code_version\": \"\{escape_json(info.code_version())}\",\n"
      out <+ "      \"command\": \"\{escape_json(info.command())}\",\n"
      out <+ "      \"random_seed\": "
      match info.random_seed() {
        Some(seed) => out <+ "\{seed},\n"
        None => out <+ "null,\n"
      }
      out <+ "      \"environment\": {"
      let env = info.environment()
      let env_keys : Array[String] = []
      env.each(fn(k, _v) { env_keys.push(k) })
      for i = 0; i < env_keys.length(); i = i + 1 {
        if i > 0 {
          out <+ ", "
        }
        let k = env_keys[i]
        let v = match env.get(k) {
          Some(v) => v
          None => ""
        }
        out <+ "\"\{escape_json(k)}\": \"\{escape_json(v)}\""
      }
      out <+ "},\n"
      let deps = info.dependencies()
      out <+ "      \"dependencies\": ["
      for i = 0; i < deps.length(); i = i + 1 {
        if i > 0 {
          out <+ ", "
        }
        out <+ "\"\{escape_json(deps[i])}\""
      }
      out <+ "]\n"
      out <+ "    }\n"
    }
    None => out <+ "    \"reproducibility\": null\n"
  }
  out <+ "  }"
}

///|
/// Export metrics for all runs in an experiment as CSV.
pub fn TrackingStore::metrics_csv(
  self : TrackingStore,
  experiment_id : String,
) -> Result[String, TrackingError] {
  // Verify experiment exists
  match self.get_experiment(experiment_id) {
    Err(err) => return Err(err)
    Ok(_) => ()
  }
  let runs = self.runs_for_experiment(experiment_id)
  // Collect all unique metric keys across all runs
  let all_keys : Array[String] = []
  for run in runs {
    for m in run.metrics() {
      if !array_contains_string(all_keys, m.key()) {
        all_keys.push(m.key())
      }
    }
  }
  let out = StringBuilder()
  // Header
  out <+ "run_id,status"
  for key in all_keys {
    out <+ ",\{key}"
  }
  out <+ "\n"
  // Rows
  for run in runs {
    out <+ "\{run.id()},\{run.status().label()}"
    for key in all_keys {
      match run.latest_metric(key) {
        Some(m) => out <+ ",\{m.value()}"
        None => out <+ ","
      }
    }
    out <+ "\n"
  }
  Ok(out.to_string())
}

// ---------------------------------------------------------------------------
// JSON helpers
// ---------------------------------------------------------------------------

///|
/// Escape a string for safe JSON output.
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()
}

///|
/// Return a single hex digit for values 0..15.
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"
    _ => "f"
  }
}