///|
fn format_scaled(value : Int64, precision : Int) -> String {
  let raw = value.to_string()
  let negative = raw.has_prefix("-")
  let digits = if negative { raw.sub(start=1).to_owned() } else { raw }
  if precision == 0 {
    if negative {
      "-" + digits
    } else {
      digits
    }
  } else {
    let padded = digits.pad_start(precision + 1, '0')
    let split = padded.length() - precision
    let whole = padded.sub(end=split).to_owned()
    let fraction = padded.sub(start=split).to_owned()
    if negative {
      "-" + whole + "." + fraction
    } else {
      whole + "." + fraction
    }
  }
}

///|
fn direction_name(direction : @evidence.Direction) -> String {
  match direction {
    @evidence.Above => "above"
    @evidence.Below => "below"
    @evidence.SameSide => "same-side"
  }
}

///|
fn scope_name(scope : @evidence.RuleScope) -> String {
  match scope {
    @evidence.SingleObservation => "single-observation"
    @evidence.SameRun => "same-run"
    @evidence.ConsecutiveRuns => "consecutive-runs"
  }
}

///|
fn json_number(value : Int64) -> Json {
  Json::number(value.to_double(), repr=value.to_string())
}

///|
fn json_issue(issue : @evidence.InputIssue) -> Json {
  let fields : Map[String, Json] = Map([])
  fields["code"] = Json::string(issue.code.name())
  fields["run_id"] = match issue.run_id {
    Some(run_id) => Json::string(run_id)
    None => Json::null()
  }
  fields["message"] = Json::string(issue.message)
  Json::object(fields)
}

///|
fn json_evidence(point : @evidence.EvidencePoint) -> Json {
  let fields : Map[String, Json] = Map([])
  fields["run_id"] = Json::string(point.run_id)
  fields["epoch_id"] = Json::string(point.epoch_id)
  fields["sequence"] = json_number(point.sequence)
  fields["timestamp"] = Json::string(point.timestamp)
  fields["control_level_id"] = Json::string(point.control_level_id)
  fields["value"] = json_number(point.value)
  fields["mean"] = json_number(point.mean)
  fields["standard_deviation"] = json_number(point.standard_deviation)
  fields["threshold_distance"] = json_number(point.threshold)
  fields["direction"] = Json::string(direction_name(point.direction))
  fields["precision"] = Json::number(point.precision.to_double())
  fields["unit"] = Json::string(point.unit)
  Json::object(fields)
}

///|
fn json_hit(hit : @evidence.RuleHit) -> Json {
  let fields : Map[String, Json] = Map([])
  fields["rule"] = Json::string(hit.rule.name())
  fields["scope"] = Json::string(scope_name(hit.scope))
  fields["reason"] = Json::string(hit.reason)
  fields["evidence"] = Json::array(hit.evidence.map(json_evidence))
  Json::object(fields)
}

///|
fn json_assessment(assessment : @evidence.RunAssessment) -> Json {
  let fields : Map[String, Json] = Map([])
  fields["run_id"] = Json::string(assessment.run_id)
  fields["sequence"] = json_number(assessment.sequence)
  fields["status"] = Json::string(assessment.status.name())
  fields["hits"] = Json::array(assessment.hits.map(json_hit))
  fields["input_issues"] = Json::array(assessment.input_issues.map(json_issue))
  Json::object(fields)
}

///|
pub fn render_trajectory_json(trajectory : @evidence.Trajectory) -> String {
  let fields : Map[String, Json] = Map([])
  fields["assessments"] = Json::array(
    trajectory.assessments.map(json_assessment),
  )
  fields["input_issues"] = Json::array(trajectory.input_issues.map(json_issue))
  Json::object(fields).stringify(indent=2)
}

///|
fn render_assessment_details_markdown(
  trajectory : @evidence.Trajectory,
) -> String {
  let output = StringBuilder()
  for assessment in trajectory.assessments {
    output.write_string("## Run ")
    output.write_string(assessment.run_id)
    output.write_string(" (sequence ")
    output.write_string(assessment.sequence.to_string())
    output.write_string(") — ")
    output.write_string(assessment.status.name())
    output.write_string("\n\n")
    if assessment.hits.length() == 0 {
      output.write_string("No rule hits.\n\n")
    } else {
      for hit in assessment.hits {
        output.write_string("- **")
        output.write_string(hit.rule.name())
        output.write_string("** (")
        output.write_string(scope_name(hit.scope))
        output.write_string("): ")
        output.write_string(hit.reason)
        output.write_string("\n")
        for point in hit.evidence {
          output.write_string("  - ")
          output.write_string(point.run_id)
          output.write_string(" / ")
          output.write_string(point.control_level_id)
          output.write_string(": value ")
          output.write_string(format_scaled(point.value, point.precision))
          output.write_string(" ")
          output.write_string(point.unit)
          output.write_string("; mean ")
          output.write_string(format_scaled(point.mean, point.precision))
          output.write_string("; threshold distance ")
          output.write_string(format_scaled(point.threshold, point.precision))
          output.write_string(" (")
          output.write_string(direction_name(point.direction))
          output.write_string(")\n")
        }
      }
      output.write_string("\n")
    }
    for input_issue in assessment.input_issues {
      output.write_string("- Input issue [")
      output.write_string(input_issue.code.name())
      output.write_string("]: ")
      output.write_string(input_issue.message)
      output.write_string("\n")
    }
    if assessment.input_issues.length() > 0 {
      output.write_string("\n")
    }
  }
  for input_issue in trajectory.input_issues {
    output.write_string("- Track issue [")
    output.write_string(input_issue.code.name())
    output.write_string("]: ")
    output.write_string(input_issue.message)
    output.write_string("\n")
  }
  output.to_string()
}

///|
pub fn render_trajectory_markdown(trajectory : @evidence.Trajectory) -> String {
  "# QC replay\n\n" + render_assessment_details_markdown(trajectory)
}