///|
/// Structured input-contract issue used by production pipelines.
pub enum ValidationSeverity {
  Info
  Warning
  Error
}

///|
pub struct ValidationIssue {
  code : String
  field : String
  message : String
  severity : ValidationSeverity
  index : Int
}

///|
pub struct ValidationReport {
  name : String
  checked : Int
  passed : Int
  failed : Int
  issues : Array[ValidationIssue]
  score : Double
}

///|
pub fn validation_issue(
  code : String,
  field : String,
  message : String,
  severity : ValidationSeverity,
  index : Int,
) -> ValidationIssue {
  { code, field, message, severity, index }
}

///|
pub fn validation_empty_report(name : String) -> ValidationReport {
  { name, checked: 0, passed: 0, failed: 0, issues: [], score: 1.0 }
}

///|
fn validation_add(
  report : ValidationReport,
  issue : ValidationIssue,
  passed : Bool,
) -> ValidationReport {
  report.issues.push(issue)
  let checked = report.checked + 1
  let failed = if passed { report.failed } else { report.failed + 1 }
  let success = if passed { report.passed + 1 } else { report.passed }
  {
    ..report,
    checked,
    passed: success,
    failed,
    score: success.to_double() / checked.to_double(),
  }
}

///|
pub fn validation_severity_name(severity : ValidationSeverity) -> String {
  match severity {
    Info => "info"
    Warning => "warning"
    Error => "error"
  }
}

///|
pub fn validation_report_lines(report : ValidationReport) -> Array[String] {
  let result = [
    "name=" + report.name,
    "checked=" + report.checked.to_string(),
    "passed=" + report.passed.to_string(),
    "failed=" + report.failed.to_string(),
    "score=" + report.score.to_string(),
  ]
  for issue in report.issues {
    result.push(
      issue.code +
      "|" +
      issue.field +
      "|" +
      validation_severity_name(issue.severity) +
      "|" +
      issue.message,
    )
  }
  result
}

///|
pub fn validation_report_string(report : ValidationReport) -> String {
  validation_report_lines(report).join("\n")
}

///|
pub fn validation_has_errors(report : ValidationReport) -> Bool {
  for issue in report.issues {
    match issue.severity {
      Error => return true
      _ => ()
    }
  }
  false
}

///|
pub fn validation_missing(
  data : Array[Double],
  name : String,
) -> ValidationReport {
  let mut report = validation_empty_report(name)
  for index = 0; index < data.length(); index = index + 1 {
    let missing = quality_is_missing(data[index])
    report = validation_add(
      report,
      validation_issue(
        "missing",
        "value",
        "missing numeric observation",
        Error,
        index,
      ),
      !missing,
    )
  }
  report
}

///|
pub fn validation_finite(
  data : Array[Double],
  name : String,
) -> ValidationReport {
  let mut report = validation_empty_report(name)
  for index = 0; index < data.length(); index = index + 1 {
    let finite = !quality_is_missing(data[index]) &&
      data[index] < 1.0e308 &&
      data[index] > -1.0e308
    report = validation_add(
      report,
      validation_issue("finite", "value", "value must be finite", Error, index),
      finite,
    )
  }
  report
}

///|
pub fn validation_range(
  data : Array[Double],
  lower : Double,
  upper : Double,
  name : String,
) -> ValidationReport {
  let mut report = validation_empty_report(name)
  for index = 0; index < data.length(); index = index + 1 {
    let valid = !quality_is_missing(data[index]) &&
      data[index] >= lower &&
      data[index] <= upper
    report = validation_add(
      report,
      validation_issue(
        "range",
        "value",
        "value is outside configured bounds",
        Error,
        index,
      ),
      valid,
    )
  }
  report
}

///|
pub fn validation_monotone(
  data : Array[Double],
  increasing : Bool,
  name : String,
) -> ValidationReport {
  let mut report = validation_empty_report(name)
  if data.length() < 2 {
    return report
  }
  for index = 1; index < data.length(); index = index + 1 {
    let valid = if increasing {
      data[index] >= data[index - 1]
    } else {
      data[index] <= data[index - 1]
    }
    report = validation_add(
      report,
      validation_issue(
        "monotone",
        "sequence",
        "sequence order violated",
        Warning,
        index,
      ),
      valid,
    )
  }
  report
}

///|
pub fn validation_length(
  left : Array[Double],
  right : Array[Double],
  name : String,
) -> ValidationReport {
  let mut report = validation_empty_report(name)
  let valid = left.length() == right.length()
  report = validation_add(
    report,
    validation_issue(
      "length",
      "series",
      "paired series have different lengths",
      Error,
      -1,
    ),
    valid,
  )
  report
}

///|
pub fn validation_pairs(
  x : Array[Double],
  y : Array[Double],
  name : String,
) -> ValidationReport {
  let mut report = validation_length(x, y, name)
  let count = if x.length() < y.length() { x.length() } else { y.length() }
  for index = 0; index < count; index = index + 1 {
    let valid = !quality_is_missing(x[index]) && !quality_is_missing(y[index])
    report = validation_add(
      report,
      validation_issue(
        "pair",
        "observation",
        "paired value is missing",
        Error,
        index,
      ),
      valid,
    )
  }
  report
}

///|
pub fn validation_weights(
  weights : Array[Double],
  name : String,
) -> ValidationReport {
  let mut report = validation_empty_report(name)
  for index = 0; index < weights.length(); index = index + 1 {
    let valid = !quality_is_missing(weights[index]) && weights[index] >= 0.0
    report = validation_add(
      report,
      validation_issue(
        "weight",
        "weight",
        "weight must be non-negative",
        Error,
        index,
      ),
      valid,
    )
  }
  let mut total = 0.0
  for weight in weights {
    total += weight
  }
  report = validation_add(
    report,
    validation_issue(
      "weight-total",
      "weights",
      "weight total must be positive",
      Error,
      -1,
    ),
    total > 0.0,
  )
  report
}

///|
pub fn validation_probability(
  probabilities : Array[Double],
  name : String,
) -> ValidationReport {
  let mut report = validation_empty_report(name)
  for index = 0; index < probabilities.length(); index = index + 1 {
    let valid = probabilities[index] >= 0.0 &&
      probabilities[index] <= 1.0 &&
      !quality_is_missing(probabilities[index])
    report = validation_add(
      report,
      validation_issue(
        "probability",
        "probability",
        "probability must be in [0,1]",
        Error,
        index,
      ),
      valid,
    )
  }
  report
}

///|
pub fn validation_matrix(
  matrix : Array[Array[Double]],
  name : String,
) -> ValidationReport {
  let mut report = validation_empty_report(name)
  let width = if matrix.length() == 0 { 0 } else { matrix[0].length() }
  report = validation_add(
    report,
    validation_issue(
      "matrix-width",
      "matrix",
      "matrix rows have inconsistent width",
      Error,
      -1,
    ),
    width > 0 || matrix.length() == 0,
  )
  for row_index = 0; row_index < matrix.length(); row_index = row_index + 1 {
    let row = matrix[row_index]
    let valid = row.length() == width
    report = validation_add(
      report,
      validation_issue(
        "matrix-row",
        "matrix",
        "matrix row width mismatch",
        Error,
        row_index,
      ),
      valid,
    )
    for value in row {
      report = validation_add(
        report,
        validation_issue(
          "matrix-finite",
          "matrix",
          "matrix value is missing",
          Error,
          row_index,
        ),
        !quality_is_missing(value),
      )
    }
  }
  report
}

///|
pub fn validation_time_axis(
  times : Array[Double],
  strict : Bool,
  name : String,
) -> ValidationReport {
  let mut report = validation_empty_report(name)
  if times.length() < 2 {
    return report
  }
  for index = 1; index < times.length(); index = index + 1 {
    let valid = if strict {
      times[index] > times[index - 1]
    } else {
      times[index] >= times[index - 1]
    }
    report = validation_add(
      report,
      validation_issue(
        "time-order",
        "timestamp",
        "timestamps are not ordered",
        Error,
        index,
      ),
      valid,
    )
  }
  report
}

///|
pub fn validation_duplicates(
  data : Array[Double],
  name : String,
) -> ValidationReport {
  let mut report = validation_empty_report(name)
  let duplicates = duplicate_indices(data)
  for index in duplicates {
    report = validation_add(
      report,
      validation_issue(
        "duplicate",
        "value",
        "duplicate observation",
        Warning,
        index,
      ),
      false,
    )
  }
  if data.length() == 0 {
    report
  } else {
    validation_add(
      report,
      validation_issue(
        "duplicate-rate",
        "series",
        "duplicate rate is measurable",
        Info,
        -1,
      ),
      true,
    )
  }
}

///|
pub fn validation_consistency(
  data : Array[Double],
  rule : QualityRule,
  name : String,
) -> ValidationReport {
  let mut report = validation_empty_report(name)
  let flags = quality_valid_flags(data, rule)
  for index = 0; index < flags.length(); index = index + 1 {
    report = validation_add(
      report,
      validation_issue(
        "quality",
        "value",
        "quality rule violated",
        Warning,
        index,
      ),
      flags[index],
    )
  }
  report
}

///|
pub fn validation_merge(
  first : ValidationReport,
  second : ValidationReport,
) -> ValidationReport {
  let issues = first.issues.copy()
  for issue in second.issues {
    issues.push(issue)
  }
  let checked = first.checked + second.checked
  let passed = first.passed + second.passed
  {
    name: first.name + "+" + second.name,
    checked,
    passed,
    failed: first.failed + second.failed,
    issues,
    score: if checked == 0 {
      1.0
    } else {
      passed.to_double() / checked.to_double()
    },
  }
}

///|
pub fn validation_batch_score(reports : Array[ValidationReport]) -> Double {
  let mut checked = 0
  let mut passed = 0
  for report in reports {
    checked += report.checked
    passed += report.passed
  }
  if checked == 0 {
    1.0
  } else {
    passed.to_double() / checked.to_double()
  }
}

///|
pub fn validation_summary(
  data : Array[Double],
  name : String,
) -> ValidationReport {
  validation_merge(
    validation_finite(data, name),
    validation_duplicates(data, name),
  )
}