///|
pub fn inspection_report(
  part_id : String,
  image : GrayImage,
  measurements : Array[Measurement],
  diagnostics : Array[PipelineDiagnostic],
) -> MeasurementReport {
  let notes : Array[String] = []
  notes.push(diagnostics_to_markdown(diagnostics))
  notes.push(
    "image=" + image.width.to_string() + "x" + image.height.to_string(),
  )
  { part_id, measurements, notes }
}

///|
pub fn inspect_rectangle(
  _image : GrayImage,
  bounds : Bounds,
  nominal_width : Double,
  nominal_height : Double,
  scale : Double,
) -> PipelineResult {
  let width = bounding_width(bounds) * scale
  let height = bounding_height(bounds) * scale
  let width_tolerance = Tolerance::Tolerance(
    nominal=nominal_width,
    lower=nominal_width * 0.99,
    upper=nominal_width * 1.01,
    unit="mm",
  )
  let height_tolerance = Tolerance::Tolerance(
    nominal=nominal_height,
    lower=nominal_height * 0.99,
    upper=nominal_height * 1.01,
    unit="mm",
  )
  suite_result(
    apply_tolerances(
      [
        {
          name: "rectangle-width",
          value: width,
          unit: "mm",
          decision: Pass,
          deviation: width - nominal_width,
        },
        {
          name: "rectangle-height",
          value: height,
          unit: "mm",
          decision: Pass,
          deviation: height - nominal_height,
        },
      ],
      [width_tolerance, height_tolerance],
    ),
  )
}

///|
pub fn inspect_circle(
  image : GrayImage,
  center : Point,
  radius : Double,
  threshold : Double,
  scale : Double,
  nominal_diameter : Double,
) -> PipelineResult {
  let mask = threshold_image(image, threshold)
  let component = largest_component(mask)
  let points = component_pixels(mask, component.label)
  let circle = if points.length() >= 3 {
    detect_circle_from_points(points)
  } else {
    Circle::Circle(center~, radius~, rms_error=0.0)
  }
  let tolerance = Tolerance::Tolerance(
    nominal=nominal_diameter,
    lower=nominal_diameter * 0.99,
    upper=nominal_diameter * 1.01,
    unit="mm",
  )
  let measurement = analyze_gap(
    "circle-diameter",
    measure_diameter(circle) * scale,
    1.0,
    tolerance,
  )
  suite_result([measurement])
}

///|
pub fn inspect_slot(
  image : GrayImage,
  bounds : Bounds,
  scale : Double,
  nominal : Double,
) -> PipelineResult {
  let mask = threshold_image(
    crop_image(image, bounds),
    otsu_threshold(crop_image(image, bounds)),
  )
  let components = label_components(mask)
  let largest = largest_component(mask)
  let value = if components.is_empty() {
    0.0
  } else {
    bounding_width(largest.bounds) * scale
  }
  let tolerance = Tolerance::Tolerance(
    nominal~,
    lower=nominal * 0.98,
    upper=nominal * 1.02,
    unit="mm",
  )
  suite_result([analyze_gap("slot-width", value, 1.0, tolerance)])
}

///|
pub fn inspect_edge_pair(
  image : GrayImage,
  config : CaliperConfig,
  scale : Double,
  tolerance : Tolerance,
) -> PipelineResult {
  measure_caliper_with_scale(image, config, scale, tolerance)
}

///|
pub fn inspect_batch(
  images : Array[GrayImage],
  config : CaliperConfig,
  tolerance : Tolerance,
) -> BatchRun {
  process_frames(images, config, tolerance)
}

///|
pub fn inspection_acceptance(
  result : PipelineResult,
  minimum_confidence : Double,
) -> Bool {
  result.accepted &&
  result.confidence >= minimum_confidence.clamp(min=0.0, max=1.0) &&
  diagnostics_ok(result.diagnostics)
}

///|
pub fn inspection_status(result : PipelineResult) -> String {
  if inspection_acceptance(result, 0.5) {
    "ACCEPT"
  } else {
    "REJECT"
  }
}

///|
pub fn inspection_summary(result : PipelineResult) -> String {
  inspection_status(result) +
  "; confidence=" +
  result.confidence.to_string() +
  "; measurements=" +
  result.measurements.length().to_string() +
  "; diagnostics=" +
  result.diagnostics.length().to_string()
}

///|
pub fn inspection_markdown(
  part_id : String,
  image : GrayImage,
  result : PipelineResult,
) -> String {
  let report = inspection_report(
    part_id,
    image,
    result.measurements,
    result.diagnostics,
  )
  render_markdown_report(report)
}

///|
pub fn inspection_csv(result : PipelineResult) -> String {
  render_measurements_csv(result.measurements)
}

///|
pub fn inspection_diagnostics(result : PipelineResult) -> String {
  diagnostics_to_markdown(result.diagnostics)
}

///|
pub fn inspection_quality(
  image : GrayImage,
  result : PipelineResult,
) -> QualitySummary {
  let profile = summarize_profile(
    edge_profile(
      image,
      Point::Point(x=0.0, y=Double::from_int(image.height / 2)),
      Point::Point(
        x=Double::from_int((image.width - 1).max(0)),
        y=Double::from_int(image.height / 2),
      ),
    ),
    0.05,
  )
  let summary = profile_quality(profile, 2.0)
  { ..summary, confidence: summary.confidence * result.confidence }
}

///|
pub fn inspection_events(
  image : GrayImage,
  result : PipelineResult,
) -> Array[DiagnosticEvent] {
  let result_events = diagnostics_for_image(image)
  for measurement in result.measurements {
    result_events.push(
      diagnostic_event(
        "MEASUREMENT",
        measurement.name,
        "value",
        measurement.value,
      ),
    )
  }
  result_events
}

///|
pub fn inspection_fingerprint(
  image : GrayImage,
  result : PipelineResult,
) -> String {
  image_checksum(image).to_string() +
  ":" +
  result.confidence.to_string() +
  ":" +
  decision_name(pipeline_decision(result))
}

///|
pub fn inspection_compare(
  first : PipelineResult,
  second : PipelineResult,
) -> Array[Measurement] {
  measurement_delta(first.measurements, second.measurements)
}

///|
pub fn inspection_merge(
  first : PipelineResult,
  second : PipelineResult,
) -> PipelineResult {
  suite_merge(first, second)
}

///|
pub fn inspection_retry(
  image : GrayImage,
  config : CaliperConfig,
  tolerance : Tolerance,
  attempts : Int,
) -> PipelineResult {
  let mut result = process_frame(image, config, tolerance)
  let mut attempt = 1
  while !result.accepted && attempt < attempts.max(1) {
    let smoother = gaussian_blur(
      image,
      attempt,
      1.0 + Double::from_int(attempt),
    )
    result = process_frame(smoother, config, tolerance)
    attempt += 1
  }
  result
}

///|
pub fn inspection_with_preprocessing(
  image : GrayImage,
  config : CaliperConfig,
  tolerance : Tolerance,
  blur_radius : Int,
) -> PipelineResult {
  let filtered = median_filter(
    gaussian_blur(image, blur_radius, 1.0),
    radius=blur_radius,
  )
  process_frame(filtered, config, tolerance)
}

///|
pub fn inspection_with_roi(
  image : GrayImage,
  roi : Roi,
  config : CaliperConfig,
  tolerance : Tolerance,
) -> PipelineResult {
  let masked = roi.masked_image(image, 0.0)
  process_frame(masked, config, tolerance)
}

///|
pub fn inspection_with_calibration(
  image : GrayImage,
  config : CaliperConfig,
  tolerance : Tolerance,
  fit : CalibrationFit,
) -> PipelineResult {
  let raw = process_frame(image, config, tolerance)
  {
    ..raw,
    measurements: raw.measurements.map(fn(measurement) {
      calibration_apply_to_measurement(fit, measurement)
    }),
  }
}

///|
pub fn inspection_with_uncertainty(
  result : PipelineResult,
  uncertainty : UncertaintyEstimate,
  tolerance : Tolerance,
) -> PipelineResult {
  let measurements = result.measurements.map(fn(measurement) {
    {
      ..measurement,
      decision: guard_band_decision(measurement.value, tolerance, uncertainty),
    }
  })
  {
    ..result,
    measurements,
    accepted: measurements.all(fn(measurement) { measurement.decision != Fail }),
  }
}

///|
pub fn inspection_all(
  image : GrayImage,
  calipers : Array[CaliperConfig],
  tolerance : Tolerance,
  fit : CalibrationFit?,
) -> PipelineResult {
  let raw = run_caliper_suite(image, calipers, tolerance)
  match fit {
    Some(model) =>
      {
        ..raw,
        measurements: raw.measurements.map(fn(measurement) {
          calibration_apply_to_measurement(model, measurement)
        }),
      }
    None => raw
  }
}

///|
pub fn inspection_version() -> String {
  "moonbit-visual-caliper/0.2.0"
}

///|
pub fn inspection_capabilities() -> Array[String] {
  [
    "gray-image", "profiles", "edges", "morphology", "components", "contours", "calibration",
    "calipers", "tolerances", "reports", "benchmarks",
  ]
}