///|
/// Stable preflight diagnostics for callers that need to reject bad input
/// without triggering the fail-fast evaluation API.
pub(all) enum ValidationCode {
  EmptyTrace
  EmptyStep
  InvalidClipRange
  InvalidEpsilon
  InvalidSparseThreshold
} derive(Debug)

///|
pub(all) struct ValidationIssue {
  code : ValidationCode
  step_index : Int
  message : String
} derive(Debug)

///|
pub fn ValidationCode::label(self : Self) -> String {
  match self {
    EmptyTrace => "EMPTY_TRACE"
    EmptyStep => "EMPTY_STEP"
    InvalidClipRange => "INVALID_CLIP_RANGE"
    InvalidEpsilon => "INVALID_EPSILON"
    InvalidSparseThreshold => "INVALID_SPARSE_THRESHOLD"
  }
}

///|
pub fn ValidationIssue::render(self : Self) -> String {
  let location = if self.step_index < 0 {
    "trace"
  } else {
    "step \{self.step_index}"
  }
  "[\{self.code.label()}] \{location}: \{self.message}"
}

///|
pub fn validate_config_issues(config : RewardConfig) -> Array[ValidationIssue] {
  let issues = Array::new()
  if config.clip_min > config.clip_max {
    issues.push(ValidationIssue::{
      code: InvalidClipRange,
      step_index: -1,
      message: "clip_min must not exceed clip_max",
    })
  }
  if config.epsilon <= 0.0 {
    issues.push(ValidationIssue::{
      code: InvalidEpsilon,
      step_index: -1,
      message: "epsilon must be greater than zero",
    })
  }
  issues
}

///|
pub fn validate_trace_issues(
  steps : Array[RewardStep],
  config : RewardConfig,
  sparse_threshold : Double,
) -> Array[ValidationIssue] {
  let issues = validate_config_issues(config)
  if steps.length() == 0 {
    issues.push(ValidationIssue::{
      code: EmptyTrace,
      step_index: -1,
      message: "at least one reward step is required",
    })
  }
  if sparse_threshold < 0.0 || sparse_threshold > 1.0 {
    issues.push(ValidationIssue::{
      code: InvalidSparseThreshold,
      step_index: -1,
      message: "sparse threshold must be between zero and one",
    })
  }
  for step in steps {
    if step.terms.length() == 0 {
      issues.push(ValidationIssue::{
        code: EmptyStep,
        step_index: step.index,
        message: "a reward step must contain at least one term",
      })
    }
  }
  issues
}

///|
pub fn is_valid_trace(
  steps : Array[RewardStep],
  config : RewardConfig,
  sparse_threshold : Double,
) -> Bool {
  validate_trace_issues(steps, config, sparse_threshold).length() == 0
}

///|
pub fn render_validation_issues(issues : Array[ValidationIssue]) -> String {
  if issues.length() == 0 {
    "Input validation passed."
  } else {
    let mut text = "Input validation failed\n"
    for issue in issues {
      text = text + "- " + issue.render() + "\n"
    }
    text
  }
}