///|
pub(all) struct QualityThresholds {
  max_gap_ns : Int64
  min_confidence : Double
  max_confidence : Double
  min_depth : Double
  max_depth : Double
  require_camera : Bool
} derive(Debug, ToJson)

///|
pub fn default_quality_thresholds() -> QualityThresholds {
  {
    max_gap_ns: 1000000000L,
    min_confidence: 0.0,
    max_confidence: 1.0,
    min_depth: 0.0,
    max_depth: 100.0,
    require_camera: true,
  }
}

///|
pub(all) enum QualityRuleCode {
  EmptyStream
  NonMonotonic
  LargeTimestampGap
  InvalidDimensions
  InvalidRange
  MissingCalibration
  LowConfidence
  OutOfBounds
  SparseStream
} derive(Debug, Eq, ToJson)

///|
fn quality_rule_code_name(code : QualityRuleCode) -> String {
  match code {
    EmptyStream => "empty_stream"
    NonMonotonic => "non_monotonic"
    LargeTimestampGap => "large_timestamp_gap"
    InvalidDimensions => "invalid_dimensions"
    InvalidRange => "invalid_range"
    MissingCalibration => "missing_calibration"
    LowConfidence => "low_confidence"
    OutOfBounds => "out_of_bounds"
    SparseStream => "sparse_stream"
  }
}

///|
pub(all) struct RuleViolation {
  code : QualityRuleCode
  source : String
  row : Int
  detail : String
} derive(Debug, ToJson)

///|
pub fn validate_image_stream(
  frames : ArrayView[ImageFrameRef],
  thresholds : QualityThresholds,
) -> Array[RuleViolation] {
  let result = Array::new()
  if frames.is_empty() {
    result.push({
      code: EmptyStream,
      source: "images",
      row: 0,
      detail: "no frames",
    })
    return result
  }
  for i in 1.. thresholds.max_gap_ns {
      result.push({
        code: LargeTimestampGap,
        source: "images",
        row: i,
        detail: "timestamp gap exceeds threshold",
      })
    }
  }
  result
}

///|
pub fn validate_trajectory_stream(
  samples : ArrayView[TrajectorySample],
  thresholds : QualityThresholds,
) -> Array[RuleViolation] {
  let result = Array::new()
  if samples.is_empty() {
    result.push({
      code: EmptyStream,
      source: "trajectory",
      row: 0,
      detail: "no samples",
    })
    return result
  }
  for i, sample in samples {
    if sample.frame_id.is_empty() {
      result.push({
        code: InvalidRange,
        source: "trajectory",
        row: i,
        detail: "empty frame id",
      })
    }
    if i > 0 && sample.stamp.compare(samples[i - 1].stamp) < 0 {
      result.push({
        code: NonMonotonic,
        source: "trajectory",
        row: i,
        detail: "timestamp moved backwards",
      })
    }
    if i > 0 &&
      sample.stamp.to_nanoseconds() - samples[i - 1].stamp.to_nanoseconds() >
      thresholds.max_gap_ns {
      result.push({
        code: LargeTimestampGap,
        source: "trajectory",
        row: i,
        detail: "timestamp gap exceeds threshold",
      })
    }
  }
  result
}

///|
pub fn validate_depth_stream(
  samples : ArrayView[DepthMetadata],
  thresholds : QualityThresholds,
) -> Array[RuleViolation] {
  let result = Array::new()
  if samples.is_empty() {
    result.push({
      code: EmptyStream,
      source: "depth",
      row: 0,
      detail: "no samples",
    })
    return result
  }
  for i, sample in samples {
    if sample.width <= 0 || sample.height <= 0 {
      result.push({
        code: InvalidDimensions,
        source: "depth",
        row: i,
        detail: "dimensions must be positive",
      })
    }
    if sample.min_depth < thresholds.min_depth ||
      sample.max_depth > thresholds.max_depth ||
      sample.min_depth > sample.max_depth {
      result.push({
        code: InvalidRange,
        source: "depth",
        row: i,
        detail: "depth range violates policy",
      })
    }
  }
  result
}

///|
pub fn validate_annotation_stream(
  annotations : ArrayView[Annotation],
  image_width : Int,
  image_height : Int,
  thresholds : QualityThresholds,
) -> Array[RuleViolation] {
  let result = Array::new()
  if annotations.is_empty() {
    result.push({
      code: EmptyStream,
      source: "annotations",
      row: 0,
      detail: "no annotations",
    })
    return result
  }
  for i, annotation in annotations {
    if annotation.confidence < thresholds.min_confidence {
      result.push({
        code: LowConfidence,
        source: "annotations",
        row: i,
        detail: "confidence below threshold",
      })
    }
    if annotation.confidence > thresholds.max_confidence {
      result.push({
        code: InvalidRange,
        source: "annotations",
        row: i,
        detail: "confidence above threshold",
      })
    }
    if !annotation.bbox.is_inside_image(image_width, image_height) {
      result.push({
        code: OutOfBounds,
        source: "annotations",
        row: i,
        detail: "box outside image",
      })
    }
  }
  result
}

///|
pub fn validate_camera(
  camera : CameraIntrinsics?,
  thresholds : QualityThresholds,
) -> Array[RuleViolation] {
  let result = Array::new()
  match camera {
    None =>
      if thresholds.require_camera {
        result.push({
          code: MissingCalibration,
          source: "camera",
          row: 0,
          detail: "camera calibration is required",
        })
      }
    Some(value) => {
      if value.width <= 0 || value.height <= 0 {
        result.push({
          code: InvalidDimensions,
          source: "camera",
          row: 0,
          detail: "image dimensions must be positive",
        })
      }
      if value.k.length() != 9 || value.p.length() != 12 {
        result.push({
          code: InvalidRange,
          source: "camera",
          row: 0,
          detail: "matrix shape is invalid",
        })
      }
    }
  }
  result
}

///|
pub fn violations_to_markdown(violations : ArrayView[RuleViolation]) -> String {
  let rows = ["| source | row | code | detail |", "|---|---:|---|---|"]
  for violation in violations {
    rows.push(
      "| \{violation.source} | \{violation.row} | \{quality_rule_code_name(violation.code)} | \{violation.detail} |",
    )
  }
  rows.join("\n")
}

///|
pub fn violations_by_code(
  violations : ArrayView[RuleViolation],
) -> Map[String, Int] {
  let result : Map[String, Int] = Map([])
  for violation in violations {
    let key = quality_rule_code_name(violation.code)
    if result.contains(key) {
      result[key] = result[key] + 1
    } else {
      result[key] = 1
    }
  }
  result
}

///|
pub fn quality_score(violations : ArrayView[RuleViolation]) -> Double {
  let mut score = 100.0
  for violation in violations {
    score -= match violation.code {
      EmptyStream => 30.0
      InvalidDimensions => 15.0
      InvalidRange => 10.0
      MissingCalibration => 15.0
      NonMonotonic => 8.0
      LargeTimestampGap => 4.0
      LowConfidence => 2.0
      OutOfBounds => 5.0
      SparseStream => 3.0
    }
  }
  if score < 0.0 {
    0.0
  } else {
    score
  }
}