///|
/// Render a contract in EvoWitness's canonical, review-stable DSL form.
pub fn Contract::to_dsl(self : Contract) -> String {
  let out = StringBuilder()
  out.write_string("contract " + self.name + " " + self.version + "\n")
  for object in self.objects {
    out.write_string("\ntype " + object.name + " ")
    out.write_string(if object.open { "open\n" } else { "closed\n" })
    for field in object.fields {
      out.write_string(
        "field " + field.name + " " + field.type_expr.render() + " ",
      )
      out.write_string(if field.required { "required" } else { "optional" })
      for token in field.constraints.to_tokens() {
        out.write_string(" " + token)
      }
      out.write_string("\n")
    }
    out.write_string("end\n")
  }
  out.to_string()
}

///|
fn Constraints::to_tokens(self : Constraints) -> Array[String] {
  let tokens : Array[String] = []
  match self.min_int {
    Some(value) => tokens.push("min=" + value.to_string())
    None => ()
  }
  match self.max_int {
    Some(value) => tokens.push("max=" + value.to_string())
    None => ()
  }
  match self.min_len {
    Some(value) => tokens.push("minlen=" + value.to_string())
    None => ()
  }
  match self.max_len {
    Some(value) => tokens.push("maxlen=" + value.to_string())
    None => ()
  }
  match self.default_value {
    Some(value) => tokens.push("default=" + value)
    None => ()
  }
  tokens
}

///|
/// Parse and re-render a source document, removing comments and normalizing layout.
pub fn canonicalize(source : String) -> Result[String, ParseError] {
  match parse_contract(source) {
    Ok(contract) => Ok(contract.to_dsl())
    Err(error) => Err(error)
  }
}

///|
/// Analyze every adjacent transition in an ordered contract history.
pub fn analyze_series(
  contracts : Array[Contract],
  mode? : CompatibilityMode = Full,
) -> Array[AnalysisReport] {
  let reports : Array[AnalysisReport] = []
  if contracts.length() < 2 {
    return reports
  }
  for index = 0; index < contracts.length() - 1; index = index + 1 {
    reports.push(analyze(contracts[index], contracts[index + 1], mode~))
  }
  reports
}

///|
/// Analyze every older/newer pair in an ordered contract history.
/// This catches a sequence that is safe per-step but violates a longer support window.
pub fn analyze_support_window(
  contracts : Array[Contract],
  mode? : CompatibilityMode = Full,
) -> Array[AnalysisReport] {
  let reports : Array[AnalysisReport] = []
  for old_index = 0; old_index < contracts.length(); old_index = old_index + 1 {
    for new_index = old_index + 1
        new_index < contracts.length()
        new_index = new_index + 1 {
      reports.push(analyze(contracts[old_index], contracts[new_index], mode~))
    }
  }
  reports
}

///|
/// Highest-risk verdict across a version series.
pub fn series_verdict(reports : Array[AnalysisReport]) -> String {
  if reports.any(report => !report.is_compatible()) {
    "incompatible"
  } else {
    "compatible"
  }
}