///|
/// Difference class produced when comparing two device observations.
pub(all) enum ReconciliationKind {
  MissingPoint
  AddedPoint
  ValueChanged
  QualityChanged
  TimestampRegressed
  SnapshotComplete
} derive(Debug, Eq)

///|
/// One difference between an expected and observed snapshot.
pub(all) struct ReconciliationDiff {
  point_id : String
  kind : ReconciliationKind
  expected : Int?
  observed : Int?
  detail : String
}

///|
pub fn ReconciliationDiff::new(
  point_id : String,
  kind : ReconciliationKind,
  expected? : Int = 0,
  observed? : Int = 0,
  detail? : String = "",
) -> Result[ReconciliationDiff, ModbusError] {
  if point_id == "" {
    Err(InvalidData)
  } else {
    let selected_expected = if expected == 0 { None } else { Some(expected) }
    let selected_observed = if observed == 0 { None } else { Some(observed) }
    Ok({
      point_id,
      kind,
      expected: selected_expected,
      observed: selected_observed,
      detail,
    })
  }
}

///|
pub fn ReconciliationDiff::point_id(self : ReconciliationDiff) -> String {
  self.point_id
}

///|
pub fn ReconciliationDiff::kind(
  self : ReconciliationDiff,
) -> ReconciliationKind {
  self.kind
}

///|
pub fn ReconciliationDiff::expected(self : ReconciliationDiff) -> Int? {
  self.expected
}

///|
pub fn ReconciliationDiff::observed(self : ReconciliationDiff) -> Int? {
  self.observed
}

///|
pub fn ReconciliationDiff::detail(self : ReconciliationDiff) -> String {
  self.detail
}

///|
pub fn reconciliation_kind_name(kind : ReconciliationKind) -> String {
  match kind {
    MissingPoint => "missing"
    AddedPoint => "added"
    ValueChanged => "value-changed"
    QualityChanged => "quality-changed"
    TimestampRegressed => "timestamp-regressed"
    SnapshotComplete => "complete"
  }
}

///|
/// Compare point values and qualities in two snapshots.
pub fn reconcile_snapshots(
  expected : DeviceSnapshot,
  observed : DeviceSnapshot,
) -> Result[Array[ReconciliationDiff], ModbusError] {
  if expected.device_id() != observed.device_id() {
    return Err(UnitMismatch)
  }
  let result : Array[ReconciliationDiff] = []
  for value in expected.values {
    match observed.get(value.point_id()) {
      None => {
        let diff = match
          ReconciliationDiff::new(
            value.point_id(),
            MissingPoint,
            expected=value.value(),
            detail="not-observed",
          ) {
          Ok(item) => item
          Err(error) => return Err(error)
        }
        result.push(diff)
      }
      Some(other) => {
        if value.value() != other.value() {
          let diff = match
            ReconciliationDiff::new(
              value.point_id(),
              ValueChanged,
              expected=value.value(),
              observed=other.value(),
              detail="value-mismatch",
            ) {
            Ok(item) => item
            Err(error) => return Err(error)
          }
          result.push(diff)
        }
        if value.quality() != other.quality() {
          let diff = match
            ReconciliationDiff::new(
              value.point_id(),
              QualityChanged,
              detail="quality-mismatch",
            ) {
            Ok(item) => item
            Err(error) => return Err(error)
          }
          result.push(diff)
        }
      }
    }
  }
  for value in observed.values {
    match expected.get(value.point_id()) {
      None => {
        let diff = match
          ReconciliationDiff::new(
            value.point_id(),
            AddedPoint,
            observed=value.value(),
            detail="unexpected-point",
          ) {
          Ok(item) => item
          Err(error) => return Err(error)
        }
        result.push(diff)
      }
      Some(_) => ()
    }
  }
  if expected.complete() && observed.complete() && result.length() == 0 {
    let diff = match
      ReconciliationDiff::new(
        expected.device_id(),
        SnapshotComplete,
        detail="snapshots-equivalent",
      ) {
      Ok(item) => item
      Err(error) => return Err(error)
    }
    result.push(diff)
  }
  Ok(result)
}

///|
/// Accumulated reconciliation result for a device fleet.
pub(all) struct ReconciliationReport {
  device_id : String
  compared : Int
  differences : Int
  missing : Int
  added : Int
  changed : Int
  equivalent : Bool
}

///|
pub fn reconciliation_report(
  device_id : String,
  diffs : Array[ReconciliationDiff],
  compared : Int,
) -> Result[ReconciliationReport, ModbusError] {
  if device_id == "" || compared < 0 {
    return Err(InvalidData)
  }
  let mut missing = 0
  let mut added = 0
  let mut changed = 0
  for diff in diffs {
    match diff.kind() {
      MissingPoint => missing += 1
      AddedPoint => added += 1
      ValueChanged | QualityChanged | TimestampRegressed => changed += 1
      SnapshotComplete => ()
    }
  }
  Ok({
    device_id,
    compared,
    differences: diffs.length(),
    missing,
    added,
    changed,
    equivalent: diffs.length() == 0 ||
    (diffs.length() == 1 && diffs[0].kind() == SnapshotComplete),
  })
}

///|
pub fn ReconciliationReport::device_id(self : ReconciliationReport) -> String {
  self.device_id
}

///|
pub fn ReconciliationReport::compared(self : ReconciliationReport) -> Int {
  self.compared
}

///|
pub fn ReconciliationReport::differences(self : ReconciliationReport) -> Int {
  self.differences
}

///|
pub fn ReconciliationReport::missing(self : ReconciliationReport) -> Int {
  self.missing
}

///|
pub fn ReconciliationReport::added(self : ReconciliationReport) -> Int {
  self.added
}

///|
pub fn ReconciliationReport::changed(self : ReconciliationReport) -> Int {
  self.changed
}

///|
pub fn ReconciliationReport::equivalent(self : ReconciliationReport) -> Bool {
  self.equivalent
}