///|
fn nibble_value(char : Char) -> Int raise {
  if char >= '0' && char <= '9' {
    char.to_int() - '0'.to_int()
  } else if char >= 'a' && char <= 'f' {
    10 + char.to_int() - 'a'.to_int()
  } else if char >= 'A' && char <= 'F' {
    10 + char.to_int() - 'A'.to_int()
  } else {
    raise @frame.FrameError::InvalidHexText(char.to_string())
  }
}

///|
fn parse_hex_identifier(text : StringView) -> Int raise {
  let trimmed = text.trim()
  let body = if trimmed.has_prefix("0x") || trimmed.has_prefix("0X") {
    trimmed[2:]
  } else {
    trimmed
  }
  guard body.length() > 0 else {
    raise @frame.FrameError::InvalidHexText(text.to_owned())
  }
  let mut value = 0
  for char in body {
    value = (value << 4) + nibble_value(char)
  }
  value
}

///|
fn nibble_char(value : Int) -> Char {
  if value < 10 {
    ('0'.to_int() + value).unsafe_to_char()
  } else {
    ('A'.to_int() + value - 10).unsafe_to_char()
  }
}

///|
fn to_hex_identifier(value : Int?) -> String {
  match value {
    Some(int_value) => {
      let masked = int_value.land(0xFFFF)
      String::from_array([
        '0',
        'x',
        nibble_char((masked >> 12).land(0xF)),
        nibble_char((masked >> 8).land(0xF)),
        nibble_char((masked >> 4).land(0xF)),
        nibble_char(masked.land(0xF)),
      ])
    }
    None => ""
  }
}

///|
fn to_optional_int_text(value : Int?) -> String {
  match value {
    Some(int_value) => int_value.to_string()
    None => ""
  }
}

///|
fn assess_observed_timing(
  observed_ms : Int?,
  mandatory_ms : Int,
  recommended_ms? : Int? = None,
) -> String {
  match observed_ms {
    None => "not_measured"
    Some(value) =>
      match recommended_ms {
        Some(recommended) =>
          if value <= recommended {
            "within_recommended"
          } else if value <= mandatory_ms {
            "within_mandatory"
          } else {
            "out_of_spec"
          }
        None =>
          if value <= mandatory_ms {
            "within_mandatory"
          } else {
            "out_of_spec"
          }
      }
  }
}

///|
fn validate_device_summary_against_gsd(
  summary : @dcp.DeviceSummary,
  gsd_summary : @gsd.GsdSummary,
  observed_set_usage_ms : Int?,
  observed_ip_startup_ms : Int?,
  observed_reset_to_factory_ms : Int?,
  observed_ar_resource_release_ms : Int?,
  observed_autonegotiation_ms : Int?,
) -> String raise {
  let response_vendor_id = to_hex_identifier(summary.vendor_id)
  let response_device_id = to_hex_identifier(summary.device_id)
  let response_station_name = summary.station_name.unwrap_or("")
  let response_vendor_name = summary.vendor_name.unwrap_or("")
  let vendor_id_match = match summary.vendor_id {
    Some(value) => value == parse_hex_identifier(gsd_summary.vendor_id[:])
    None => false
  }
  let device_id_match = match summary.device_id {
    Some(value) => value == parse_hex_identifier(gsd_summary.device_id[:])
    None => false
  }
  let vendor_name_match = match summary.vendor_name {
    Some(value) => value == gsd_summary.vendor_name
    None => false
  }
  let station_name_match = if gsd_summary.dns_compatible_name == "" {
    true
  } else {
    match summary.station_name {
      Some(value) => value == gsd_summary.dns_compatible_name
      None => false
    }
  }
  let vendor_name_is_advisory_mismatch = summary.vendor_name is Some(_) &&
    !vendor_name_match &&
    vendor_id_match &&
    device_id_match &&
    station_name_match
  let set_usage_time_assessment = assess_observed_timing(
    observed_set_usage_ms,
    mandatory_set_usage_time_ms(),
  )
  let ip_startup_time_assessment = assess_observed_timing(
    observed_ip_startup_ms,
    mandatory_ip_startup_time_ms(),
    recommended_ms=Some(recommended_ip_startup_time_ms()),
  )
  let reset_to_factory_time_assessment = assess_observed_timing(
    observed_reset_to_factory_ms,
    mandatory_reset_to_factory_time_ms(),
    recommended_ms=Some(recommended_reset_to_factory_time_ms()),
  )
  let ar_resource_release_time_assessment = assess_observed_timing(
    observed_ar_resource_release_ms,
    mandatory_ar_resource_release_timeout_ms(),
    recommended_ms=Some(recommended_ar_resource_release_timeout_ms()),
  )
  let autonegotiation_time_assessment = assess_observed_timing(
    observed_autonegotiation_ms,
    mandatory_autonegotiation_timeout_ms(),
    recommended_ms=Some(recommended_autonegotiation_timeout_ms()),
  )
  let mismatch_fields : Array[String] = []
  if !vendor_id_match {
    mismatch_fields.push("vendor_id")
  }
  if !device_id_match {
    mismatch_fields.push("device_id")
  }
  if summary.vendor_name is Some(_) &&
    !vendor_name_match &&
    !vendor_name_is_advisory_mismatch {
    mismatch_fields.push("vendor_name")
  }
  if !station_name_match {
    mismatch_fields.push("station_name")
  }
  if set_usage_time_assessment == "out_of_spec" {
    mismatch_fields.push("set_usage_time")
  }
  if ip_startup_time_assessment == "out_of_spec" {
    mismatch_fields.push("ip_startup_time")
  }
  if reset_to_factory_time_assessment == "out_of_spec" {
    mismatch_fields.push("reset_to_factory_time")
  }
  if ar_resource_release_time_assessment == "out_of_spec" {
    mismatch_fields.push("ar_resource_release_timeout")
  }
  if autonegotiation_time_assessment == "out_of_spec" {
    mismatch_fields.push("autonegotiation_timeout")
  }
  let overall_match = mismatch_fields.is_empty()
  [
    "source=" + summary.source.to_hex(),
    "station_name=" + response_station_name,
    "response_vendor_id=" + response_vendor_id,
    "response_device_id=" + response_device_id,
    "response_vendor_name=" + response_vendor_name,
    "gsd_vendor_id=" + gsd_summary.vendor_id,
    "gsd_device_id=" + gsd_summary.device_id,
    "gsd_vendor_name=" + gsd_summary.vendor_name,
    "gsd_main_family=" + gsd_summary.main_family,
    "gsd_product_family=" + gsd_summary.product_family,
    "gsd_order_number=" + gsd_summary.order_number,
    "gsd_device_access_point_id=" + gsd_summary.device_access_point_id,
    "gsd_dns_compatible_name=" + gsd_summary.dns_compatible_name,
    "vendor_id_match=" + vendor_id_match.to_string(),
    "device_id_match=" + device_id_match.to_string(),
    "vendor_name_match=" + vendor_name_match.to_string(),
    "vendor_name_advisory_mismatch=" +
    vendor_name_is_advisory_mismatch.to_string(),
    "station_name_match=" + station_name_match.to_string(),
    "device_identity_match=" + (vendor_id_match && device_id_match).to_string(),
    "ip_address_change_bumpless=" + ip_address_change_bumpless().to_string(),
    "station_name_change_bumpless=" + station_name_change_bumpless().to_string(),
    "set_usage_time_mandatory_ms=" + mandatory_set_usage_time_ms().to_string(),
    "observed_set_usage_time_ms=" + to_optional_int_text(observed_set_usage_ms),
    "set_usage_time_assessment=" + set_usage_time_assessment,
    "set_usage_time_probeability=" + set_usage_time_probeability(),
    "set_usage_time_probeability_class=" + set_usage_time_probeability_class(),
    "ip_startup_time_recommended_ms=" +
    recommended_ip_startup_time_ms().to_string(),
    "ip_startup_time_mandatory_ms=" + mandatory_ip_startup_time_ms().to_string(),
    "observed_ip_startup_time_ms=" +
    to_optional_int_text(observed_ip_startup_ms),
    "ip_startup_time_assessment=" + ip_startup_time_assessment,
    "ip_startup_time_probeability=" + ip_startup_time_probeability(),
    "ip_startup_time_probeability_class=" + ip_startup_time_probeability_class(),
    "reset_to_factory_time_recommended_ms=" +
    recommended_reset_to_factory_time_ms().to_string(),
    "reset_to_factory_time_mandatory_ms=" +
    mandatory_reset_to_factory_time_ms().to_string(),
    "observed_reset_to_factory_time_ms=" +
    to_optional_int_text(observed_reset_to_factory_ms),
    "reset_to_factory_time_assessment=" + reset_to_factory_time_assessment,
    "reset_to_factory_time_probeability=" + reset_to_factory_time_probeability(),
    "reset_to_factory_time_probeability_class=" +
    reset_to_factory_time_probeability_class(),
    "ar_resource_release_time_recommended_ms=" +
    recommended_ar_resource_release_timeout_ms().to_string(),
    "ar_resource_release_time_mandatory_ms=" +
    mandatory_ar_resource_release_timeout_ms().to_string(),
    "observed_ar_resource_release_time_ms=" +
    to_optional_int_text(observed_ar_resource_release_ms),
    "ar_resource_release_time_assessment=" + ar_resource_release_time_assessment,
    "ar_resource_release_time_probeability=" +
    ar_resource_release_time_probeability(),
    "ar_resource_release_time_probeability_class=" +
    ar_resource_release_time_probeability_class(),
    "autonegotiation_time_recommended_ms=" +
    recommended_autonegotiation_timeout_ms().to_string(),
    "autonegotiation_time_mandatory_ms=" +
    mandatory_autonegotiation_timeout_ms().to_string(),
    "observed_autonegotiation_time_ms=" +
    to_optional_int_text(observed_autonegotiation_ms),
    "autonegotiation_time_assessment=" + autonegotiation_time_assessment,
    "autonegotiation_time_probeability=" + autonegotiation_time_probeability(),
    "autonegotiation_time_probeability_class=" +
    autonegotiation_time_probeability_class(),
    "mandatory_set_storage_time_ms=" +
    mandatory_set_storage_time_ms().to_string(),
    "maximum_diagnosis_data_per_submodule_octets_exclusive_upper_bound=" +
    maximum_diagnosis_data_per_submodule_octets_exclusive_upper_bound().to_string(),
    "minimum_logbook_entries=" + minimum_logbook_entries().to_string(),
    "logbook_data_probeability=" + logbook_data_probeability(),
    "logbook_data_probeability_class=" + logbook_data_probeability_class(),
    "pruning_mac_destination_support_recommended=" +
    pruning_mac_destination_support_recommended().to_string(),
    "lldp_system_capabilities_station_only_required=" +
    lldp_system_capabilities_station_only_required().to_string(),
    "lldp_msg_tx_hold=" + lldp_msg_tx_hold().to_string(),
    "lldp_msg_tx_interval_ms=" + lldp_msg_tx_interval_ms().to_string(),
    "lldp_msg_tx_interval_positive_jitter_ms=" +
    lldp_msg_tx_interval_positive_jitter_ms().to_string(),
    "lldp_tx_ttl_ms=" + lldp_tx_ttl_ms().to_string(),
    "lldp_tx_ttl_positive_jitter_ms=" +
    lldp_tx_ttl_positive_jitter_ms().to_string(),
    "lldp_tx_now_on_change=" + lldp_tx_now_on_change().to_string(),
    "lldp_minimum_neighbor_capacity=" +
    lldp_minimum_neighbor_capacity().to_string(),
    "lldp_recommended_neighbor_capacity=" +
    lldp_recommended_neighbor_capacity().to_string(),
    "lldp_neighbor_probeability=" + lldp_neighbor_probeability(),
    "lldp_neighbor_probeability_class=" + lldp_neighbor_probeability_class(),
    "minimum_arp_cache_size=" + minimum_arp_cache_size().to_string(),
    "arp_cache_timeout_ms=" + arp_cache_timeout_ms().to_string(),
    "arp_response_timeout_ms=" + arp_response_timeout_ms().to_string(),
    "dns_request_timeout_ms=" + dns_request_timeout_ms().to_string(),
    "reject_dcp_set_requests_support=" + reject_dcp_set_requests_support(),
    "snmp_service_optional=" + snmp_service_optional().to_string(),
    "snmp_default_access_disabled=" + snmp_default_access_disabled().to_string(),
    "snmp_explicit_enable_required=" +
    snmp_explicit_enable_required().to_string(),
    "snmp_disabled_after_ar_abort=" + snmp_disabled_after_ar_abort().to_string(),
    "snmp_cim_interface_overrides_ar=" +
    snmp_cim_interface_overrides_ar().to_string(),
    "snmp_community_public_access=" + snmp_community_public_access(),
    "snmp_community_private_access=" + snmp_community_private_access(),
    "snmp_set_timeout_recommended_ms=" +
    recommended_snmp_set_timeout_ms().to_string(),
    "snmp_set_timeout_mandatory_ms=" +
    mandatory_snmp_set_timeout_ms().to_string(),
    "snmp_get_timeout_recommended_ms=" +
    recommended_snmp_get_timeout_ms().to_string(),
    "snmp_get_timeout_mandatory_ms=" +
    mandatory_snmp_get_timeout_ms().to_string(),
    "snmp_get_next_timeout_recommended_ms=" +
    recommended_snmp_get_next_timeout_ms().to_string(),
    "snmp_get_next_timeout_mandatory_ms=" +
    mandatory_snmp_get_next_timeout_ms().to_string(),
    "snmp_mib_update_time_recommended_ms=" +
    recommended_snmp_mib_update_time_ms().to_string(),
    "snmp_mib_update_time_mandatory_ms=" +
    mandatory_snmp_mib_update_time_ms().to_string(),
    "snmp_feature_mib2_required=" + snmp_feature_mib2_required().to_string(),
    "snmp_feature_lldp_mib_required=" +
    snmp_feature_lldp_mib_required().to_string(),
    "snmp_feature_ieee_8021as_mib_optional=" +
    snmp_feature_ieee_8021as_mib_optional().to_string(),
    "snmp_feature_bridge_mib_optional=" +
    snmp_feature_bridge_mib_optional().to_string(),
    "snmp_feature_pno_mib_optional=" +
    snmp_feature_pno_mib_optional().to_string(),
    "snmp_default_state_probeability=" + snmp_default_state_probeability(),
    "snmp_default_state_probeability_class=" +
    snmp_default_state_probeability_class(),
    "snmp_timeout_probeability=" + snmp_timeout_probeability(),
    "snmp_timeout_probeability_class=" + snmp_timeout_probeability_class(),
    "snmp_mib_update_probeability=" + snmp_mib_update_probeability(),
    "snmp_mib_update_probeability_class=" + snmp_mib_update_probeability_class(),
    "dhcp_service_optional=" + dhcp_service_optional().to_string(),
    "dhcp_subnet_mask_option_required=" +
    dhcp_subnet_mask_option_required().to_string(),
    "dhcp_gateway_option_required=" + dhcp_gateway_option_required().to_string(),
    "dhcp_parameter_data_required_value=" +
    dhcp_parameter_data_required_value().to_string(),
    "dhcp_other_options_optional=" + dhcp_other_options_optional().to_string(),
    "dhcp_option_support_probeability=" + dhcp_option_support_probeability(),
    "dhcp_option_support_probeability_class=" +
    dhcp_option_support_probeability_class(),
    "dhcp_runtime_state_probeability=" + dhcp_runtime_state_probeability(),
    "dhcp_runtime_state_probeability_class=" +
    dhcp_runtime_state_probeability_class(),
    "overall_match=" + overall_match.to_string(),
    "mismatch_fields=" + mismatch_fields.join(","),
  ].join("\n")
}

///|
fn format_live_validation_report(
  operation : String,
  observation : LiveSetObservation,
  validation : String?,
) -> String {
  let lines : Array[String] = []
  lines.push("operation=" + operation)
  lines.push("response_text_begin")
  lines.push(observation.response_text)
  lines.push("response_text_end")
  lines.push(
    "target_state_observed=" +
    (observation.observed_set_usage_ms is Some(_)).to_string(),
  )
  match observation.observed_set_usage_ms {
    Some(value) => lines.push("observed_set_usage_time_ms=" + value.to_string())
    None => lines.push("observed_set_usage_time_ms=")
  }
  match validation {
    Some(report) => {
      lines.push("")
      lines.push(report)
    }
    None => lines.push("validation_status=target_state_not_observed")
  }
  lines.join("\n")
}

///|
pub fn parse_gsdml_summary_file(path : StringView) -> String raise {
  format_gsd_summary(@gsd.parse_gsdml_summary_file(path))
}

///|
pub fn validate_device_fixture_against_gsd_file(
  packet_hex : StringView,
  gsd_path : StringView,
) -> String raise {
  validate_device_fixture_against_gsd_file_with_timings(packet_hex, gsd_path)
}

///|
pub fn validate_device_fixture_against_gsd_file_with_timings(
  packet_hex : StringView,
  gsd_path : StringView,
  observed_set_usage_ms? : Int? = None,
  observed_ip_startup_ms? : Int? = None,
  observed_reset_to_factory_ms? : Int? = None,
  observed_ar_resource_release_ms? : Int? = None,
  observed_autonegotiation_ms? : Int? = None,
) -> String raise {
  let response = @dcp.parse_response(@frame.bytes_from_hex(packet_hex))
  let summary = @dcp.summarize_response(response)
  let gsd_summary = @gsd.parse_gsdml_summary_file(gsd_path)
  validate_device_summary_against_gsd(
    summary, gsd_summary, observed_set_usage_ms, observed_ip_startup_ms, observed_reset_to_factory_ms,
    observed_ar_resource_release_ms, observed_autonegotiation_ms,
  )
}

///|
pub fn validate_scanned_devices_against_gsd_file(
  interface_name : StringView,
  gsd_path : StringView,
  timeout_ms? : Int = default_dcp_identify_timeout_ms(),
  xid? : Int = 16909060,
) -> String {
  validate_scanned_devices_against_gsd_file_with_timings(
    interface_name,
    gsd_path,
    timeout_ms~,
    xid~,
  )
}

///|
pub fn validate_scanned_devices_against_gsd_file_with_timings(
  interface_name : StringView,
  gsd_path : StringView,
  timeout_ms? : Int = default_dcp_identify_timeout_ms(),
  xid? : Int = 16909060,
  observed_set_usage_ms? : Int? = None,
  observed_ip_startup_ms? : Int? = None,
  observed_reset_to_factory_ms? : Int? = None,
  observed_ar_resource_release_ms? : Int? = None,
  observed_autonegotiation_ms? : Int? = None,
) -> String {
  root_facade_native_handoff_text(
    "validate_scanned_devices_against_gsd_file_with_timings",
    "@rawnet.RawSocket::open + DCP Identify + GSD validation",
    [
      "interface=" + interface_name.to_owned(),
      "gsd_path=" + gsd_path.to_owned(),
      "timeout_ms=" + timeout_ms.to_string(),
      "xid=" + xid.to_string(),
      "observed_set_usage_time_ms=" +
      to_optional_int_text(observed_set_usage_ms),
      "observed_ip_startup_time_ms=" +
      to_optional_int_text(observed_ip_startup_ms),
      "observed_reset_to_factory_time_ms=" +
      to_optional_int_text(observed_reset_to_factory_ms),
      "observed_ar_resource_release_time_ms=" +
      to_optional_int_text(observed_ar_resource_release_ms),
      "observed_autonegotiation_time_ms=" +
      to_optional_int_text(observed_autonegotiation_ms),
      "gsd_validation_materialized=false",
    ],
  )
}

///|
pub fn validate_live_set_name_against_gsd_file(
  interface_name : StringView,
  destination_mac : StringView,
  station_name : String,
  gsd_path : StringView,
  timeout_ms? : Int = default_dcp_set_get_timeout_ms(),
  observe_window_ms? : Int = 2000,
  xid? : Int = 16909060,
) -> String raise {
  let observation = live_set_station_name_observation(
    interface_name,
    destination_mac,
    station_name,
    timeout_ms~,
    observe_window_ms~,
    xid~,
  )
  let validation = match observation.observed_summary {
    Some(summary) => {
      let gsd_summary = @gsd.parse_gsdml_summary_file(gsd_path)
      Some(
        validate_device_summary_against_gsd(
          summary,
          gsd_summary,
          observation.observed_set_usage_ms,
          None,
          None,
          None,
          None,
        ),
      )
    }
    None => None
  }
  format_live_validation_report("set-name", observation, validation)
}

///|
pub fn validate_live_set_ip_against_gsd_file(
  interface_name : StringView,
  destination_mac : StringView,
  address : StringView,
  mask : StringView,
  gateway : StringView,
  gsd_path : StringView,
  timeout_ms? : Int = default_dcp_set_get_timeout_ms(),
  observe_window_ms? : Int = 2000,
  xid? : Int = 16909060,
) -> String raise {
  let observation = live_set_ip_parameters_observation(
    interface_name,
    destination_mac,
    address,
    mask,
    gateway,
    timeout_ms~,
    observe_window_ms~,
    xid~,
  )
  let validation = match observation.observed_summary {
    Some(summary) => {
      let gsd_summary = @gsd.parse_gsdml_summary_file(gsd_path)
      Some(
        validate_device_summary_against_gsd(
          summary,
          gsd_summary,
          observation.observed_set_usage_ms,
          None,
          None,
          None,
          None,
        ),
      )
    }
    None => None
  }
  format_live_validation_report("set-ip", observation, validation)
}