///|
/// Semantic value families recognized by MoonLogfmt Lens.
///
/// The classifier is intentionally deterministic and dependency-free. It does
/// not try to replace a full date, network, or identity parser. Its purpose is
/// to give contract inference and drift analysis a stable vocabulary.
pub(all) enum ValueKind {
  ValueFlag
  ValueEmpty
  ValueBoolean
  ValueInteger
  ValueDecimal
  ValueDuration
  ValueByteSize
  ValueTimestamp
  ValueIPv4
  ValueUuid
  ValueEmail
  ValueHex
  ValueIdentifier
  ValueText
} derive(Eq, Debug)

///|
pub fn ValueKind::label(self : ValueKind) -> String {
  match self {
    ValueFlag => "flag"
    ValueEmpty => "empty"
    ValueBoolean => "boolean"
    ValueInteger => "integer"
    ValueDecimal => "decimal"
    ValueDuration => "duration"
    ValueByteSize => "byte_size"
    ValueTimestamp => "timestamp"
    ValueIPv4 => "ipv4"
    ValueUuid => "uuid"
    ValueEmail => "email"
    ValueHex => "hex"
    ValueIdentifier => "identifier"
    ValueText => "text"
  }
}

///|
/// Returns true when values of `actual` are accepted by a rule for `expected`.
///
/// Integer values are accepted by decimal rules, while every value can be
/// represented as text. Empty and flag values remain explicit so callers can
/// decide whether they are allowed.
pub fn ValueKind::accepts(self : ValueKind, actual : ValueKind) -> Bool {
  if self == actual {
    true
  } else {
    match self {
      ValueDecimal => actual == ValueInteger
      ValueText => actual != ValueFlag
      _ => false
    }
  }
}

///|
/// A compact distribution of value kinds.
pub struct ValueDistribution {
  flag_count : Int
  empty_count : Int
  boolean_count : Int
  integer_count : Int
  decimal_count : Int
  duration_count : Int
  byte_size_count : Int
  timestamp_count : Int
  ipv4_count : Int
  uuid_count : Int
  email_count : Int
  hex_count : Int
  identifier_count : Int
  text_count : Int
} derive(Eq, Debug)

///|
pub fn ValueDistribution::empty() -> ValueDistribution {
  {
    flag_count: 0,
    empty_count: 0,
    boolean_count: 0,
    integer_count: 0,
    decimal_count: 0,
    duration_count: 0,
    byte_size_count: 0,
    timestamp_count: 0,
    ipv4_count: 0,
    uuid_count: 0,
    email_count: 0,
    hex_count: 0,
    identifier_count: 0,
    text_count: 0,
  }
}

///|
pub fn ValueDistribution::total(self : ValueDistribution) -> Int {
  self.flag_count +
  self.empty_count +
  self.boolean_count +
  self.integer_count +
  self.decimal_count +
  self.duration_count +
  self.byte_size_count +
  self.timestamp_count +
  self.ipv4_count +
  self.uuid_count +
  self.email_count +
  self.hex_count +
  self.identifier_count +
  self.text_count
}

///|
pub fn ValueDistribution::count(
  self : ValueDistribution,
  kind : ValueKind,
) -> Int {
  match kind {
    ValueFlag => self.flag_count
    ValueEmpty => self.empty_count
    ValueBoolean => self.boolean_count
    ValueInteger => self.integer_count
    ValueDecimal => self.decimal_count
    ValueDuration => self.duration_count
    ValueByteSize => self.byte_size_count
    ValueTimestamp => self.timestamp_count
    ValueIPv4 => self.ipv4_count
    ValueUuid => self.uuid_count
    ValueEmail => self.email_count
    ValueHex => self.hex_count
    ValueIdentifier => self.identifier_count
    ValueText => self.text_count
  }
}

///|
pub fn ValueDistribution::with_kind(
  self : ValueDistribution,
  kind : ValueKind,
) -> ValueDistribution {
  let flag_delta = if kind == ValueFlag { 1 } else { 0 }
  let empty_delta = if kind == ValueEmpty { 1 } else { 0 }
  let boolean_delta = if kind == ValueBoolean { 1 } else { 0 }
  let integer_delta = if kind == ValueInteger { 1 } else { 0 }
  let decimal_delta = if kind == ValueDecimal { 1 } else { 0 }
  let duration_delta = if kind == ValueDuration { 1 } else { 0 }
  let byte_size_delta = if kind == ValueByteSize { 1 } else { 0 }
  let timestamp_delta = if kind == ValueTimestamp { 1 } else { 0 }
  let ipv4_delta = if kind == ValueIPv4 { 1 } else { 0 }
  let uuid_delta = if kind == ValueUuid { 1 } else { 0 }
  let email_delta = if kind == ValueEmail { 1 } else { 0 }
  let hex_delta = if kind == ValueHex { 1 } else { 0 }
  let identifier_delta = if kind == ValueIdentifier { 1 } else { 0 }
  let text_delta = if kind == ValueText { 1 } else { 0 }
  {
    flag_count: self.flag_count + flag_delta,
    empty_count: self.empty_count + empty_delta,
    boolean_count: self.boolean_count + boolean_delta,
    integer_count: self.integer_count + integer_delta,
    decimal_count: self.decimal_count + decimal_delta,
    duration_count: self.duration_count + duration_delta,
    byte_size_count: self.byte_size_count + byte_size_delta,
    timestamp_count: self.timestamp_count + timestamp_delta,
    ipv4_count: self.ipv4_count + ipv4_delta,
    uuid_count: self.uuid_count + uuid_delta,
    email_count: self.email_count + email_delta,
    hex_count: self.hex_count + hex_delta,
    identifier_count: self.identifier_count + identifier_delta,
    text_count: self.text_count + text_delta,
  }
}

///|
pub fn ValueDistribution::dominant_kind(self : ValueDistribution) -> ValueKind {
  let order = [
    ValueFlag,
    ValueEmpty,
    ValueBoolean,
    ValueInteger,
    ValueDecimal,
    ValueDuration,
    ValueByteSize,
    ValueTimestamp,
    ValueIPv4,
    ValueUuid,
    ValueEmail,
    ValueHex,
    ValueIdentifier,
    ValueText,
  ]
  let mut best = ValueText
  let mut best_count = -1
  for kind in order {
    let count = self.count(kind)
    if count > best_count {
      best = kind
      best_count = count
    }
  }
  best
}

///|
pub fn ValueDistribution::dominant_percent(self : ValueDistribution) -> Int {
  if self.total() == 0 {
    0
  } else {
    self.count(self.dominant_kind()) * 100 / self.total()
  }
}

///|
pub fn ValueDistribution::distinct_kind_count(self : ValueDistribution) -> Int {
  let order = [
    ValueFlag,
    ValueEmpty,
    ValueBoolean,
    ValueInteger,
    ValueDecimal,
    ValueDuration,
    ValueByteSize,
    ValueTimestamp,
    ValueIPv4,
    ValueUuid,
    ValueEmail,
    ValueHex,
    ValueIdentifier,
    ValueText,
  ]
  let mut count = 0
  for kind in order {
    if self.count(kind) > 0 {
      count = count + 1
    }
  }
  count
}

///|
pub fn ValueDistribution::summary(self : ValueDistribution) -> String {
  let parts : Array[String] = []
  let order = [
    ValueFlag,
    ValueEmpty,
    ValueBoolean,
    ValueInteger,
    ValueDecimal,
    ValueDuration,
    ValueByteSize,
    ValueTimestamp,
    ValueIPv4,
    ValueUuid,
    ValueEmail,
    ValueHex,
    ValueIdentifier,
    ValueText,
  ]
  for kind in order {
    let count = self.count(kind)
    if count > 0 {
      parts.push(kind.label() + "=" + count.to_string())
    }
  }
  parts.join(",")
}

///|
/// Classifies a parsed field while preserving the distinction between an
/// implicit flag and an explicit boolean value.
pub fn classify_field(field : Field) -> ValueKind {
  if field.is_flag() {
    ValueFlag
  } else {
    classify_value(field.value())
  }
}

///|
/// Classifies one logfmt value using a stable, ordered set of recognizers.
pub fn classify_value(value : String) -> ValueKind {
  if value == "" {
    ValueEmpty
  } else if profile_is_boolean(value) {
    ValueBoolean
  } else if profile_is_integer(value) {
    ValueInteger
  } else if profile_is_decimal(value) {
    ValueDecimal
  } else if profile_is_duration(value) {
    ValueDuration
  } else if profile_is_byte_size(value) {
    ValueByteSize
  } else if profile_is_timestamp(value) {
    ValueTimestamp
  } else if profile_is_ipv4(value) {
    ValueIPv4
  } else if profile_is_uuid(value) {
    ValueUuid
  } else if profile_is_email(value) {
    ValueEmail
  } else if profile_is_hex(value) {
    ValueHex
  } else if profile_is_identifier(value) {
    ValueIdentifier
  } else {
    ValueText
  }
}

///|
pub fn profile_values(values : Array[String]) -> ValueDistribution {
  let mut distribution = ValueDistribution::empty()
  for value in values {
    distribution = distribution.with_kind(classify_value(value))
  }
  distribution
}

///|
fn profile_ascii_lower(input : String) -> String {
  let output : Array[Char] = []
  for char in input.to_array() {
    if char >= 'A' && char <= 'Z' {
      output.push((char.to_int() + 32).unsafe_to_char())
    } else {
      output.push(char)
    }
  }
  String::from_array(output)
}

///|
fn profile_is_boolean(value : String) -> Bool {
  let lower = profile_ascii_lower(value)
  lower == "true" ||
  lower == "false" ||
  lower == "yes" ||
  lower == "no" ||
  lower == "on" ||
  lower == "off"
}

///|
fn profile_is_integer(value : String) -> Bool {
  let chars = value.to_array()
  if chars.length() == 0 {
    return false
  }
  let mut index = 0
  if chars[0] == '+' || chars[0] == '-' {
    if chars.length() == 1 {
      return false
    }
    index = 1
  }
  while index < chars.length() {
    if !profile_is_digit(chars[index]) {
      return false
    }
    index = index + 1
  }
  true
}

///|
fn profile_is_decimal(value : String) -> Bool {
  let chars = value.to_array()
  if chars.length() < 3 {
    return false
  }
  let mut index = 0
  if chars[0] == '+' || chars[0] == '-' {
    index = 1
  }
  let mut dots = 0
  let mut digits_before = 0
  let mut digits_after = 0
  while index < chars.length() {
    let char = chars[index]
    if char == '.' {
      dots = dots + 1
      if dots > 1 {
        return false
      }
    } else if profile_is_digit(char) {
      if dots == 0 {
        digits_before = digits_before + 1
      } else {
        digits_after = digits_after + 1
      }
    } else {
      return false
    }
    index = index + 1
  }
  dots == 1 && digits_before > 0 && digits_after > 0
}

///|
fn profile_is_duration(value : String) -> Bool {
  let lower = profile_ascii_lower(value)
  let suffixes = ["ns", "us", "ms", "s", "m", "h", "d"]
  for suffix in suffixes {
    if profile_has_suffix(lower, suffix) {
      let chars = lower.to_array()
      let suffix_length = suffix.to_array().length()
      let number = String::from_array(chars[0:chars.length() - suffix_length])
      return profile_is_integer(number) || profile_is_decimal(number)
    }
  }
  false
}

///|
fn profile_is_byte_size(value : String) -> Bool {
  let lower = profile_ascii_lower(value)
  let suffixes = ["kib", "mib", "gib", "tib", "kb", "mb", "gb", "tb", "b"]
  for suffix in suffixes {
    if profile_has_suffix(lower, suffix) {
      let chars = lower.to_array()
      let suffix_length = suffix.to_array().length()
      let number = String::from_array(chars[0:chars.length() - suffix_length])
      return profile_is_integer(number) || profile_is_decimal(number)
    }
  }
  false
}

///|
fn profile_is_timestamp(value : String) -> Bool {
  let chars = value.to_array()
  if chars.length() < 10 {
    return false
  }
  if chars[4] != '-' || chars[7] != '-' {
    return false
  }
  let digit_positions = [0, 1, 2, 3, 5, 6, 8, 9]
  for position in digit_positions {
    if !profile_is_digit(chars[position]) {
      return false
    }
  }
  if chars.length() == 10 {
    return true
  }
  if chars.length() < 16 || (chars[10] != 'T' && chars[10] != ' ') {
    return false
  }
  profile_is_digit(chars[11]) &&
  profile_is_digit(chars[12]) &&
  chars[13] == ':' &&
  profile_is_digit(chars[14]) &&
  profile_is_digit(chars[15])
}

///|
fn profile_is_ipv4(value : String) -> Bool {
  let chars = value.to_array()
  let mut groups = 0
  let mut digits = 0
  let mut number = 0
  for char in chars {
    if profile_is_digit(char) {
      digits = digits + 1
      if digits > 3 {
        return false
      }
      number = number * 10 + char.to_int() - '0'.to_int()
      if number > 255 {
        return false
      }
    } else if char == '.' {
      if digits == 0 {
        return false
      }
      groups = groups + 1
      digits = 0
      number = 0
    } else {
      return false
    }
  }
  groups == 3 && digits > 0
}

///|
fn profile_is_uuid(value : String) -> Bool {
  let chars = value.to_array()
  if chars.length() != 36 {
    return false
  }
  for index = 0; index < chars.length(); index = index + 1 {
    if index == 8 || index == 13 || index == 18 || index == 23 {
      if chars[index] != '-' {
        return false
      }
    } else if !profile_is_hex_digit(chars[index]) {
      return false
    }
  }
  true
}

///|
fn profile_is_email(value : String) -> Bool {
  let chars = value.to_array()
  let mut at = -1
  let mut dot_after = false
  for index = 0; index < chars.length(); index = index + 1 {
    let char = chars[index]
    if char == '@' {
      if at >= 0 || index == 0 || index == chars.length() - 1 {
        return false
      }
      at = index
    } else if char == '.' && at >= 0 && index > at + 1 {
      dot_after = true
    } else if is_space(char) {
      return false
    }
  }
  at > 0 && dot_after
}

///|
fn profile_is_hex(value : String) -> Bool {
  let chars = value.to_array()
  let mut start = 0
  if chars.length() >= 3 &&
    (profile_has_prefix(value, "0x") || profile_has_prefix(value, "0X")) {
    start = 2
  } else if chars.length() < 8 {
    return false
  }
  if start >= chars.length() {
    return false
  }
  for index = start; index < chars.length(); index = index + 1 {
    if !profile_is_hex_digit(chars[index]) {
      return false
    }
  }
  true
}

///|
fn profile_is_identifier(value : String) -> Bool {
  let chars = value.to_array()
  if chars.length() == 0 {
    return false
  }
  let mut has_letter = false
  for char in chars {
    if (char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') {
      has_letter = true
    } else if !profile_is_digit(char) &&
      char != '_' &&
      char != '-' &&
      char != '.' &&
      char != '/' &&
      char != ':' {
      return false
    }
  }
  has_letter
}

///|
fn profile_is_digit(char : Char) -> Bool {
  char >= '0' && char <= '9'
}

///|
fn profile_is_hex_digit(char : Char) -> Bool {
  profile_is_digit(char) ||
  (char >= 'a' && char <= 'f') ||
  (char >= 'A' && char <= 'F')
}

///|
fn profile_has_prefix(value : String, prefix : String) -> Bool {
  let chars = value.to_array()
  let prefix_chars = prefix.to_array()
  if prefix_chars.length() > chars.length() {
    return false
  }
  for index = 0; index < prefix_chars.length(); index = index + 1 {
    if chars[index] != prefix_chars[index] {
      return false
    }
  }
  true
}

///|
fn profile_has_suffix(value : String, suffix : String) -> Bool {
  let chars = value.to_array()
  let suffix_chars = suffix.to_array()
  if suffix_chars.length() > chars.length() {
    return false
  }
  let start = chars.length() - suffix_chars.length()
  for index = 0; index < suffix_chars.length(); index = index + 1 {
    if chars[start + index] != suffix_chars[index] {
      return false
    }
  }
  true
}