///|
pub(all) suberror LifecycleError {
  CleanupFailed(Error?, Error)
}

///|
pub(all) struct Failure {
  phase : String
  cause : Error?
  cleanup : Error?
}

///|
pub(all) struct Terminal {
  id : String
  status : String
}

///|
pub fn Failure::cancelled(self : Failure) -> Bool {
  self.cause is Some(@protocol.Invalid("Cancelled"))
}

///|
fn bounded_error(error : Error) -> String {
  let text = match error {
    @protocol.Invalid(message)
    | @config.Invalid(message)
    | @scheme.Invalid(message) => message
    @data.Invalid(code, message) => code + ": " + message
    @moonbit.Invalid(code, context, message) =>
      code + ": " + context + " " + message
    _ => error.to_string()
  }
  if text.length() > 512 {
    text[:512].to_owned()
  } else {
    text
  }
}

///|
pub fn Failure::to_json(self : Failure) -> Json {
  {
    "phase": self.phase,
    "cause": self.cause
    .map(e => Json::string(bounded_error(e)))
    .unwrap_or(Json::null()),
    "cleanup": self.cleanup
    .map(e => Json::string(bounded_error(e)))
    .unwrap_or(Json::null()),
  }
}

///|
/// A required matrix cannot infer success from a partial set or matching errors.
pub fn matrix_successful(
  required : Array[String],
  reports : Array[Report],
) -> Bool {
  if required.is_empty() || required.length() != reports.length() {
    return false
  }
  let seen = Map([])
  for target in required {
    if seen.contains(target) {
      return false
    }
    seen[target] = true
    let lane = reports.filter(r => r.target == target)
    if lane.length() != 1 || !lane[0].successful() || lane[0].evaluator is None {
      return false
    }
    if lane[0].evaluator != reports[0].evaluator ||
      lane[0].comparison.to_json() != reports[0].comparison.to_json() {
      return false
    }
    if lane[0].terminals.map(t => t.id) != reports[0].terminals.map(t => t.id) {
      return false
    }
  }
  true
}

///|
/// Native rich values and opaque annotations remain Ion in verification receipts.
pub fn Report::to_ion(self : Report) -> @ion.IonValue raise {
  guard self.to_json() is Object(fields) else { abort("report object") }
  ignore(fields.remove("calls"))
  ignore(fields.remove("extensions"))
  let names = fields.keys().collect()
  names.sort()
  let result = names.map(name => (name, evidence_ion(fields[name])))
  result.push(
    (
      "calls",
      @ion.IonValue::list(
        self.calls.map(c => {
          @protocol.record([
            ("id", @ion.IonValue::string(c.id)),
            ("actual", c.actual.to_ion()),
            (
              "expected",
              c.expected
              .map(v => v.to_ion())
              .unwrap_or(@ion.IonValue::untyped_null()),
            ),
            ("matches", @ion.IonValue::bool(c.matches)),
            (
              "mismatch_path",
              c.mismatch_path
              .map(@ion.IonValue::string)
              .unwrap_or(@ion.IonValue::untyped_null()),
            ),
          ])
        }),
      ),
    ),
  )
  result.push(
    (
      "extensions",
      match self.extensions {
        Some(bytes) =>
          @ion_binary.read_binary_value(
            bytes[:],
            fuel=Limited(@ion.Budget(128)),
          )
        None => @ion.IonValue::untyped_null()
      },
    ),
  )
  @protocol.record(result)
}

///|
fn evidence_ion(value : Json) -> @ion.IonValue raise {
  match value {
    Null => @ion.IonValue::untyped_null()
    String(s) => @ion.IonValue::string(s)
    True => @ion.IonValue::bool(true)
    False => @ion.IonValue::bool(false)
    Number(n, ..) => @ion.IonValue::float(n)
    Array(items) => @ion.IonValue::list(items.map(evidence_ion))
    Object(fields) => {
      let names = fields.keys().collect()
      names.sort()
      @protocol.record(names.map(name => (name, evidence_ion(fields[name]))))
    }
  }
}

///|
pub fn Report::status(self : Report) -> String {
  if self.failure is Some(f) {
    return if f.cancelled() { "cancelled" } else { "failed" }
  }
  if self.successful() {
    "succeeded"
  } else if self.terminals.any(t => t.status == "mismatch") {
    "mismatch"
  } else {
    "failed"
  }
}