///|
pub(all) enum ValidationKind {
  EmptyName
  InvalidTarget
  InvalidWindow
  InvalidRule
  EmptyCollection
} derive(Eq, Debug)

///|
pub(all) struct ValidationIssue {
  code : String
  message : String
  kind : ValidationKind
} derive(Eq, Debug)

///|
pub(all) struct ValidationReport {
  mut valid : Bool
  mut checked : Int
  issues : Array[ValidationIssue]
} derive(Eq, Debug)

///|
fn empty_report() -> ValidationReport {
  { valid: true, checked: 0, issues: [] }
}

///|
fn issue(
  report : ValidationReport,
  kind : ValidationKind,
  code : String,
  message : String,
) -> ValidationReport {
  let issues = report.issues
  issues.push({ code, message, kind })
  { ..report, valid: false, issues }
}

///|
fn checked(report : ValidationReport) -> ValidationReport {
  { ..report, checked: report.checked + 1 }
}

///|
pub fn ValidationReport::has_code(
  self : ValidationReport,
  code : String,
) -> Bool {
  for item in self.issues {
    if item.code == code {
      return true
    }
  }
  false
}

///|
pub fn ValidationReport::issue_count(self : ValidationReport) -> Int {
  self.issues.length()
}

///|
pub fn ValidationReport::summary(self : ValidationReport) -> String {
  if self.valid {
    "valid: \{self.checked} checks"
  } else {
    "invalid: \{self.issues.length()} issues across \{self.checked} checks"
  }
}

///|
pub fn validate_target(target : SloTarget) -> ValidationReport {
  let report = checked(empty_report())
  let report = if target.name.length() == 0 {
    issue(report, EmptyName, "empty_name", "SLO target name must not be empty")
  } else {
    report
  }
  if target.target_bp < 0 ||
    target.target_bp > 10000 ||
    target.period_minutes <= 0 {
    issue(
      report,
      InvalidTarget,
      "invalid_target",
      "target basis points and period must be in range",
    )
  } else {
    report
  }
}

///|
pub fn validate_window(window : RequestWindow) -> ValidationReport {
  let report = checked(empty_report())
  let report = if window.name.length() == 0 {
    issue(
      report,
      EmptyName,
      "empty_name",
      "request window name must not be empty",
    )
  } else {
    report
  }
  if window.minutes <= 0 ||
    window.total < 0 ||
    window.good < 0 ||
    window.good > window.total {
    issue(
      report,
      InvalidWindow,
      "invalid_window",
      "window counts and duration are inconsistent",
    )
  } else {
    report
  }
}

///|
pub fn validate_rule(rule : BurnRule) -> ValidationReport {
  let report = checked(empty_report())
  if rule.name.length() == 0 || rule.threshold_x100 <= 0 {
    issue(
      report,
      InvalidRule,
      "invalid_rule",
      "burn rule needs a name and positive threshold",
    )
  } else {
    report
  }
}

///|
pub fn validate_samples(samples : Array[TrafficSample]) -> ValidationReport {
  let mut report = empty_report()
  if samples.length() == 0 {
    report = issue(
      report,
      EmptyCollection,
      "empty_samples",
      "at least one traffic sample is required",
    )
    report
  } else {
    for sample in samples {
      let _ = checked(report)
      if sample.total < 0 || sample.good < 0 || sample.good > sample.total {
        report.valid = false
        report.issues.push({
          code: "invalid_sample",
          message: "sample good count must be between zero and total",
          kind: InvalidWindow,
        })
      }
    }
    report
  }
}

///|
pub fn validate_config(config : SloConfig) -> ValidationReport {
  let target_report = validate_target(config.target)
  let mut report = target_report
  if config.windows.length() == 0 {
    report = issue(
      report,
      EmptyCollection,
      "empty_windows",
      "configuration needs at least one window",
    )
    ignore(report)
  } else {
    for policy in config.windows {
      let _ = checked(report)
      if policy.minutes <= 0 || policy.name.length() == 0 {
        report.valid = false
        report.issues.push({
          code: "invalid_policy_window",
          message: "window policy needs a name and positive duration",
          kind: InvalidWindow,
        })
      }
    }
  }
  if config.rules.length() == 0 {
    report = issue(
      report,
      EmptyCollection,
      "empty_rules",
      "configuration needs at least one burn rule",
    )
    ignore(report)
  } else {
    for rule in config.rules {
      let rule_report = validate_rule(rule)
      report.checked = report.checked + rule_report.checked
      for item in rule_report.issues {
        report.issues.push(item)
      }
      if !rule_report.valid {
        report.valid = false
      }
    }
  }
  report
}

///|
pub fn validate_windows(windows : Array[RequestWindow]) -> ValidationReport {
  let mut report = empty_report()
  if windows.length() == 0 {
    report = issue(
      report,
      EmptyCollection,
      "empty_windows",
      "at least one request window is required",
    )
    ignore(report)
  } else {
    for window in windows {
      let item = validate_window(window)
      report.checked = report.checked + item.checked
      if !item.valid {
        report.valid = false
      }
      for problem in item.issues {
        report.issues.push(problem)
      }
    }
  }
  report
}

///|
pub fn merge_validation(
  a : ValidationReport,
  b : ValidationReport,
) -> ValidationReport {
  let result = {
    valid: a.valid && b.valid,
    checked: a.checked + b.checked,
    issues: [],
  }
  for item in a.issues {
    result.issues.push(item)
  }
  for item in b.issues {
    result.issues.push(item)
  }
  result
}