///|
/// Declarative validation rule for incoming analysis data.
pub struct ContractRule {
  name : String
  required : Bool
  minimum : Double
  maximum : Double
  allow_missing : Bool
}

///|
/// One data-contract violation.
pub struct ContractViolation {
  rule : String
  row : Int
  column : Int
  value : Double
  message : String
}

///|
/// Data-contract audit result.
pub struct ContractAudit {
  rows : Int
  columns : Int
  violations : Array[ContractViolation]
  checked_cells : Int
  score : Double
  passes : Bool
}

///|
/// Creates a numeric range rule.
pub fn contract_rule(
  name : String,
  minimum? : Double = -1.0e300,
  maximum? : Double = 1.0e300,
  required? : Bool = false,
  allow_missing? : Bool = true,
) -> ContractRule {
  {
    name,
    required,
    minimum: minimum.min(maximum),
    maximum: maximum.max(minimum),
    allow_missing,
  }
}

///|
/// Validates matrix cells against per-column rules.
pub fn audit_data_contract(
  matrix : Array[Array[Double]],
  rules : Array[ContractRule],
) -> ContractAudit {
  let violations : Array[ContractViolation] = Array::new()
  let mut checked = 0
  for row_index in 0.. rule.maximum) {
        violations.push({
          rule: rule.name,
          row: row_index,
          column: column_index,
          value,
          message: "value outside contract range",
        })
      }
    }
  }
  let score = if checked == 0 {
    0.0
  } else {
    clamp(1.0 - violations.length().to_double() / checked.to_double(), 0.0, 1.0)
  }
  {
    rows: matrix.length(),
    columns: rules.length(),
    violations,
    checked_cells: checked,
    score,
    passes: violations.length() == 0,
  }
}

///|
/// Validates monotonic integer time within each unit.
pub fn audit_monotonic_panel(
  unit : Array[Int],
  time : Array[Int],
) -> Array[ContractViolation] {
  let result : Array[ContractViolation] = Array::new()
  let n = unit.length().min(time.length())
  for i in 1.. Array[ContractViolation] {
  let result : Array[ContractViolation] = Array::new()
  let n = unit.length().min(time.length())
  for i in 0.. ContractAudit {
  let treated = treatment.filter(fn(value) { value }).length()
  let control = treatment.length() - treated
  let violations : Array[ContractViolation] = Array::new()
  if treated == 0 {
    violations.push({
      rule: "treatment-support",
      row: -1,
      column: -1,
      value: 0.0,
      message: "no treated observations",
    })
  }
  if control == 0 {
    violations.push({
      rule: "treatment-support",
      row: -1,
      column: -1,
      value: 1.0,
      message: "no control observations",
    })
  }
  {
    rows: treatment.length(),
    columns: 1,
    violations,
    checked_cells: treatment.length(),
    score: if violations.length() == 0 {
      1.0
    } else {
      0.0
    },
    passes: violations.length() == 0,
  }
}

///|
/// Merges multiple contract violations into an audit.
pub fn merge_contract_audits(
  first : ContractAudit,
  second : ContractAudit,
) -> ContractAudit {
  let violations = first.violations.copy()
  for violation in second.violations {
    violations.push(violation)
  }
  let checked = first.checked_cells + second.checked_cells
  {
    rows: first.rows.max(second.rows),
    columns: first.columns.max(second.columns),
    violations,
    checked_cells: checked,
    score: if checked == 0 {
      0.0
    } else {
      clamp(
        1.0 - violations.length().to_double() / checked.to_double(),
        0.0,
        1.0,
      )
    },
    passes: violations.length() == 0,
  }
}

///|
/// Returns a compact data-contract summary vector.
pub fn contract_summary(audit : ContractAudit) -> Array[Double] {
  [
    audit.rows.to_double(),
    audit.columns.to_double(),
    audit.violations.length().to_double(),
    audit.checked_cells.to_double(),
    audit.score,
    if audit.passes {
      1.0
    } else {
      0.0
    },
  ]
}