///|
/// Shared validation and normalization primitives for engineering inputs.
pub(all) struct ValidationIssue {
  field : String
  code : String
  message : String
  value : Double
  blocking : Bool
} derive(Debug, Eq, ToJson)

///|
pub(all) struct ValidationSummary {
  checked : Int
  passed : Int
  issues : Array[ValidationIssue]
  valid : Bool
  completeness : Double
} derive(Debug, Eq, ToJson)

///|
pub fn validation_issue(
  field : String,
  code : String,
  message : String,
  value : Double,
  blocking : Bool,
) -> ValidationIssue {
  { field, code, message, value, blocking }
}

///|
pub fn check_finite(field : String, value : Double) -> ValidationIssue? {
  if value.is_nan() || value.is_inf() {
    Some(
      validation_issue(field, "non-finite", "value must be finite", value, true),
    )
  } else {
    None
  }
}

///|
pub fn check_positive(field : String, value : Double) -> ValidationIssue? {
  if value <= 0.0 {
    Some(
      validation_issue(
        field, "non-positive", "value must be greater than zero", value, true,
      ),
    )
  } else {
    None
  }
}

///|
pub fn check_non_negative(field : String, value : Double) -> ValidationIssue? {
  if value < 0.0 {
    Some(
      validation_issue(
        field, "negative", "value cannot be negative", value, true,
      ),
    )
  } else {
    None
  }
}

///|
pub fn check_fraction(field : String, value : Double) -> ValidationIssue? {
  if value < 0.0 || value > 1.0 {
    Some(
      validation_issue(
        field, "fraction", "value must lie between zero and one", value, true,
      ),
    )
  } else {
    None
  }
}

///|
pub fn check_range(
  field : String,
  value : Double,
  lower : Double,
  upper : Double,
) -> ValidationIssue? {
  if value < lower || value > upper {
    Some(
      validation_issue(
        field, "range", "value is outside the allowed range", value, true,
      ),
    )
  } else {
    None
  }
}

///|
pub fn check_close(
  field : String,
  value : Double,
  reference : Double,
  tolerance : Double,
) -> ValidationIssue? {
  if (value - reference).abs() > tolerance {
    Some(
      validation_issue(
        field, "mismatch", "value is not within tolerance", value, false,
      ),
    )
  } else {
    None
  }
}

///|
pub fn summarize_validation(
  checked : Int,
  issues : ArrayView[ValidationIssue],
) -> ValidationSummary {
  let mut blocking = 0
  for issue in issues {
    if issue.blocking {
      blocking += 1
    }
  }
  {
    checked,
    passed: (checked - issues.length()).max(0),
    issues: issues.to_owned(),
    valid: blocking == 0,
    completeness: if checked <= 0 {
      0.0
    } else {
      (checked - issues.length()).max(0).to_double() / checked.to_double()
    },
  }
}

///|
pub fn validate_vector(
  field_prefix : String,
  values : ArrayView[Double],
  lower : Double,
  upper : Double,
) -> ValidationSummary {
  let issues = []
  for index in 0.. issues.push(issue)
      None =>
        match
          check_range(
            field_prefix + "[" + index.to_string() + "]",
            values[index],
            lower,
            upper,
          ) {
          Some(issue) => issues.push(issue)
          None => ()
        }
    }
  }
  summarize_validation(values.length(), issues)
}

///|
pub fn normalize_to_range(
  value : Double,
  lower : Double,
  upper : Double,
) -> Double raise ExtractionError {
  if lower >= upper {
    raise ExtractionError::InvalidArgument(
      message="normalization range is invalid",
    )
  }
  (value - lower) / (upper - lower)
}

///|
pub fn denormalize_from_range(
  normalized : Double,
  lower : Double,
  upper : Double,
) -> Double raise ExtractionError {
  if lower >= upper {
    raise ExtractionError::InvalidArgument(
      message="denormalization range is invalid",
    )
  }
  lower + normalized * (upper - lower)
}

///|
pub fn clamp(value : Double, lower : Double, upper : Double) -> Double {
  value.max(lower).min(upper)
}

///|
pub fn clamp_vector(
  values : ArrayView[Double],
  lower : Double,
  upper : Double,
) -> Array[Double] {
  [
    for value in values => clamp(value, lower, upper)
  ]
}

///|
pub fn relative_difference(value : Double, reference : Double) -> Double {
  if reference == 0.0 {
    value.abs()
  } else {
    (value - reference) / reference.abs()
  }
}

///|
pub fn percent_difference(value : Double, reference : Double) -> Double {
  100.0 * relative_difference(value, reference)
}

///|
pub fn all_finite(values : ArrayView[Double]) -> Bool {
  for value in values {
    if value.is_nan() || value.is_inf() {
      return false
    }
  }
  true
}

///|
pub fn all_in_range(
  values : ArrayView[Double],
  lower : Double,
  upper : Double,
) -> Bool {
  for value in values {
    if value < lower || value > upper {
      return false
    }
  }
  true
}

///|
pub fn sum_non_negative(
  values : ArrayView[Double],
) -> Double raise ExtractionError {
  let mut total = 0.0
  for value in values {
    if value < 0.0 {
      raise ExtractionError::InvalidArgument(
        message="negative value in non-negative sum",
      )
    }
    total += value
  }
  total
}

///|
pub fn normalized_weights(
  weights : ArrayView[Double],
) -> Array[Double] raise ExtractionError {
  let total = sum_non_negative(weights)
  if total == 0.0 {
    raise ExtractionError::InvalidArgument(message="weights sum to zero")
  }
  [
    for weight in weights => weight / total
  ]
}

///|
pub fn weighted_average(
  values : ArrayView[Double],
  weights : ArrayView[Double],
) -> Double raise ExtractionError {
  if values.length() != weights.length() || values.length() == 0 {
    raise ExtractionError::InvalidArgument(
      message="weighted average arrays are invalid",
    )
  }
  let normalized = normalized_weights(weights)
  let mut total = 0.0
  for index in 0.. Double raise ExtractionError {
  let mean = weighted_average(values, weights)
  let normalized = normalized_weights(weights)
  let mut total = 0.0
  for index in 0.. String {
  let lines = [
    "| Field | Code | Message | Value | Blocking |", "|---|---|---|---:|---|",
  ]
  for issue in issues {
    lines.push(
      "| " +
      issue.field +
      " | " +
      issue.code +
      " | " +
      issue.message +
      " | " +
      issue.value.to_string() +
      " | " +
      issue.blocking.to_string() +
      " |",
    )
  }
  lines.join("\n")
}

///|
pub fn validation_summary_text(summary : ValidationSummary) -> String {
  "checked=" +
  summary.checked.to_string() +
  ",passed=" +
  summary.passed.to_string() +
  ",issues=" +
  summary.issues.length().to_string() +
  ",valid=" +
  summary.valid.to_string() +
  ",completeness=" +
  summary.completeness.to_string()
}

///|
pub fn merge_validation_summaries(
  first : ValidationSummary,
  second : ValidationSummary,
) -> ValidationSummary {
  let issues = first.issues.copy()
  issues.append(second.issues)
  summarize_validation(first.checked + second.checked, issues)
}

///|
pub fn require_valid(
  summary : ValidationSummary,
) -> Result[Unit, Array[ValidationIssue]] {
  if summary.valid {
    Ok(())
  } else {
    Err(summary.issues)
  }
}