///|
/// CLI validate command: check that discovered slaves match an expected
/// configuration, verifying identity and ESM readiness.

///|
/// ValidationResult describes the outcome of one slave check.
pub(all) enum ValidationResult {
  /// Slave matches expected configuration.
  Ok
  /// Slave station address mismatch.
  StationAddressMismatch(position~ : Int, expected~ : UInt, actual~ : UInt)
  /// Slave alias address mismatch.
  AliasAddressMismatch(position~ : Int, expected~ : UInt, actual~ : UInt)
  /// Slave identity mismatch.
  IdentityMismatch(expected~ : @hal.SlaveIdentity, actual~ : @hal.SlaveIdentity)
  /// Slave explicit identification mismatch.
  IdentificationMismatch(position~ : Int, expected~ : UInt, actual~ : UInt)
  /// Slave process-data/config summary mismatch.
  ProcessDataMismatch(position~ : Int)
  /// Slave not found at expected position.
  Missing(position~ : Int)
  /// Optional slave group member is absent.
  OptionalMissing(position~ : Int, group_id~ : UInt?)
  /// Extra slave found that was not expected.
  Extra(position~ : Int)
} derive(Debug)

///|
pub(all) enum ValidateGrade {
  Pass
  Warn
  Block
} derive(Eq, Debug)

///|
pub(all) enum ValidateDifferenceCategory {
  Topology
  Identity
  Configuration
  Informational
} derive(Eq, Debug)

///|
/// ValidateReport holds the full validation result.
pub(all) struct ValidateReport {
  expected_source : ConfigurationSource
  actual_source : ConfigurationSource
  total_expected : Int
  total_found : Int
  results : Array[ValidationResult]
  differences : Array[ConfigurationDifference]
  all_ok : Bool
  grade : ValidateGrade
} derive(Debug)

///|
/// Validate that a scan report matches an expected slave configuration.
/// Each expected slave is checked by position.
fn zero_identity() -> @hal.SlaveIdentity {
  { vendor_id: 0U, product_code: 0U, revision: 0U, serial: 0U }
}

///|
pub fn validate_configuration(
  actual : ConfigurationModel,
  expected : ConfigurationModel,
) -> ValidateReport {
  let comparison = compare_configurations(expected, actual)
  let results : Array[ValidationResult] = []
  for exp in expected.slaves {
    let position = exp.position
    let diffs = differences_for_position(comparison.differences, position)
    if diffs.length() == 0 {
      results.push(Ok)
    } else {
      let missing_or_address = first_missing_or_address_diff(diffs)
      match missing_or_address {
        Some(diff) => results.push(configuration_difference_to_validation(diff))
        None => {
          let identity_diff = first_identity_diff(diffs)
          match identity_diff {
            Some(diff) =>
              results.push(configuration_difference_to_validation(diff))
            None => results.push(Ok)
          }
          for diff in diffs {
            match diff {
              IdentificationMismatch(..) | ProcessDataSummaryMismatch(..) =>
                results.push(configuration_difference_to_validation(diff))
              _ => ()
            }
          }
        }
      }
    }
  }
  for diff in comparison.differences {
    match diff {
      Extra(_) => results.push(configuration_difference_to_validation(diff))
      _ => ()
    }
  }
  {
    expected_source: comparison.expected_source,
    actual_source: comparison.actual_source,
    total_expected: comparison.total_expected,
    total_found: comparison.total_found,
    results,
    differences: comparison.differences,
    all_ok: comparison.all_ok,
    grade: validation_grade_from_differences(comparison.differences),
  }
}

///|
fn validation_grade_from_differences(
  differences : Array[ConfigurationDifference],
) -> ValidateGrade {
  if differences.length() == 0 {
    return Pass
  }
  for diff in differences {
    match diff {
      Extra(..) | OptionalMissing(..) => ()
      _ => return Block
    }
  }
  Warn
}

///|
pub fn validation_difference_category(
  difference : ConfigurationDifference,
) -> ValidateDifferenceCategory {
  match difference {
    Missing(..)
    | OptionalMissing(..)
    | Extra(..)
    | StationAddressMismatch(..)
    | AliasAddressMismatch(..) => Topology
    IdentityMismatch(..) => Identity
    IdentificationMismatch(..) | ProcessDataSummaryMismatch(..) => Configuration
  }
}

///|
pub fn validation_difference_grade(
  difference : ConfigurationDifference,
) -> ValidateGrade {
  match difference {
    Extra(..) | OptionalMissing(..) => Warn
    _ => Block
  }
}

///|
fn differences_for_position(
  differences : Array[ConfigurationDifference],
  wanted_position : Int,
) -> Array[ConfigurationDifference] {
  let matches : Array[ConfigurationDifference] = []
  for diff in differences {
    match diff {
      Missing(position~) if position == wanted_position => matches.push(diff)
      OptionalMissing(position~, ..) if position == wanted_position =>
        matches.push(diff)
      StationAddressMismatch(position~, ..) if position == wanted_position =>
        matches.push(diff)
      AliasAddressMismatch(position~, ..) if position == wanted_position =>
        matches.push(diff)
      IdentityMismatch(position~, ..) if position == wanted_position =>
        matches.push(diff)
      IdentificationMismatch(position~, ..) if position == wanted_position =>
        matches.push(diff)
      ProcessDataSummaryMismatch(position~, ..) if position == wanted_position =>
        matches.push(diff)
      _ => ()
    }
  }
  matches
}

///|
fn first_missing_or_address_diff(
  differences : Array[ConfigurationDifference],
) -> ConfigurationDifference? {
  for diff in differences {
    match diff {
      Missing(..)
      | OptionalMissing(..)
      | StationAddressMismatch(..)
      | AliasAddressMismatch(..) => return Some(diff)
      _ => ()
    }
  }
  None
}

///|
fn first_identity_diff(
  differences : Array[ConfigurationDifference],
) -> ConfigurationDifference? {
  for diff in differences {
    match diff {
      IdentityMismatch(..) => return Some(diff)
      _ => ()
    }
  }
  None
}

///|
fn configuration_difference_to_validation(
  difference : ConfigurationDifference,
) -> ValidationResult {
  match difference {
    Missing(position~) => Missing(position~)
    OptionalMissing(position~, group_id~) =>
      OptionalMissing(position~, group_id~)
    Extra(position~) => Extra(position~)
    StationAddressMismatch(position~, expected~, actual~) =>
      StationAddressMismatch(
        position~,
        expected=option_uint_or_zero(expected),
        actual=option_uint_or_zero(actual),
      )
    AliasAddressMismatch(position~, expected~, actual~) =>
      AliasAddressMismatch(
        position~,
        expected=option_uint_or_zero(expected),
        actual=option_uint_or_zero(actual),
      )
    IdentityMismatch(position~, expected~, actual~) => {
      let _ = position
      ValidationResult::IdentityMismatch(
        expected=option_identity_or_zero(expected),
        actual=option_identity_or_zero(actual),
      )
    }
    IdentificationMismatch(position~, expected~, actual~) =>
      IdentificationMismatch(
        position~,
        expected=option_uint_or_zero(expected),
        actual=option_uint_or_zero(actual),
      )
    ProcessDataSummaryMismatch(position~, ..) => ProcessDataMismatch(position~)
  }
}

///|
fn option_uint_or_zero(value : UInt?) -> UInt {
  match value {
    Some(number) => number
    None => 0U
  }
}

///|
fn option_identity_or_zero(value : @hal.SlaveIdentity?) -> @hal.SlaveIdentity {
  match value {
    Some(identity) => identity
    None => zero_identity()
  }
}

///|
/// Validate that scan results match expected slaves by `position`.
pub fn validate(
  scan_report : ScanReport,
  expected : Array[@hal.SlaveConfig],
) -> ValidateReport {
  validate_configuration(
    configuration_from_scan(scan_report),
    configuration_from_expected(expected),
  )
}

///|
fn validate_grade_label(grade : ValidateGrade) -> String {
  match grade {
    Pass => "PASS"
    Warn => "WARN"
    Block => "BLOCK"
  }
}

///|
/// Format a ValidateReport as a human-readable string.
pub fn ValidateReport::format(self : ValidateReport) -> String {
  let buf = StringBuilder(size_hint=256)
  buf.write_string("EtherCAT Validation Report\n")
  buf.write_string("==========================\n")
  buf.write_string(
    "Expected Source: " + @debug.to_string(self.expected_source) + "\n",
  )
  buf.write_string(
    "Actual Source:   " + @debug.to_string(self.actual_source) + "\n",
  )
  buf.write_string("Expected: " + self.total_expected.to_string() + " slaves\n")
  buf.write_string("Found:    " + self.total_found.to_string() + " slaves\n")
  buf.write_string("Status:   " + validate_grade_label(self.grade) + "\n\n")
  for i = 0; i < self.results.length(); i = i + 1 {
    match self.results[i] {
      Ok => buf.write_string("  [OK] Slave #" + i.to_string() + "\n")
      StationAddressMismatch(position~, expected~, actual~) =>
        buf.write_string(
          "  [ADDR-MISMATCH] Slave #" +
          position.to_string() +
          ": expected 0x" +
          expected.reinterpret_as_int().to_string() +
          " actual 0x" +
          actual.reinterpret_as_int().to_string() +
          "\n",
        )
      AliasAddressMismatch(position~, expected~, actual~) =>
        buf.write_string(
          "  [ALIAS-MISMATCH] Slave #" +
          position.to_string() +
          ": expected 0x" +
          expected.reinterpret_as_int().to_string() +
          " actual 0x" +
          actual.reinterpret_as_int().to_string() +
          "\n",
        )
      IdentityMismatch(..) =>
        buf.write_string(
          "  [MISMATCH] Slave #" + i.to_string() + ": identity mismatch\n",
        )
      IdentificationMismatch(position~, expected~, actual~) =>
        buf.write_string(
          "  [ID-MISMATCH] Slave #" +
          position.to_string() +
          ": expected 0x" +
          expected.reinterpret_as_int().to_string() +
          " actual 0x" +
          actual.reinterpret_as_int().to_string() +
          "\n",
        )
      ProcessDataMismatch(position~) =>
        buf.write_string(
          "  [CONFIG-MISMATCH] Slave #" +
          position.to_string() +
          ": process-data summary mismatch\n",
        )
      Missing(position~) =>
        buf.write_string(
          "  [MISSING] Slave #" + position.to_string() + ": not found\n",
        )
      OptionalMissing(position~, group_id~) =>
        buf.write_string(
          "  [OPTIONAL-MISSING] Slave #" +
          position.to_string() +
          ": optional hot-connect member absent" +
          optional_group_suffix(group_id) +
          "\n",
        )
      Extra(position~) =>
        buf.write_string(
          "  [EXTRA] Slave #" + position.to_string() + ": unexpected\n",
        )
    }
  }
  buf.to_string()
}

///|
fn optional_group_suffix(group_id : UInt?) -> String {
  match group_id {
    Some(group) => " (group=" + group.to_string() + ")"
    None => ""
  }
}