///|
/// IDNA label validation (UTS #46 Section 4.1)

///|
/// Validate a single label according to UTS #46 validity criteria
fn validate_label(
  label : String,
  use_std3_ascii_rules~ : Bool,
  check_hyphens~ : Bool,
  check_bidi~ : Bool,
  check_joiners~ : Bool,
) -> Unit raise IdnaError {
  // Empty labels should raise an error - they indicate either intentional empty
  // labels (like trailing dots) or labels that became empty after IDNA mapping
  // (like soft hyphen U+00AD). The caller can catch EmptyLabel and decide
  // whether to allow it based on context.
  if label.length() == 0 {
    raise EmptyLabel
  }
  let chars = label_to_chars(label)

  // Check 1: Must be NFC normalized
  if !@normalization.is_normalized(label, @normalization.NFC) {
    raise InvalidLabel(label)
  }

  // Check 2 & 3: Hyphen restrictions (when check_hyphens)
  if check_hyphens {
    validate_hyphens(label, chars)
  }

  // Check 4: No U+002E (full stop) within label - handled by label splitting

  // Check 5: No leading combining marks
  if chars.length() > 0 {
    let first_c = chars[0]
    if is_combining_mark(first_c) {
      raise LeadingCombiningMark
    }
  }

  // Check 6: All characters must have valid status
  for c in chars {
    validate_char_status(c, use_std3_ascii_rules)
  }

  // Check 7: ContextJ rules (when check_joiners)
  if check_joiners {
    validate_contextj(chars)
  }

  // Check 8: Bidi rules (when check_bidi)
  if check_bidi {
    validate_bidi(chars)
  }
}

///|
/// Convert a label string to array of characters
fn label_to_chars(label : String) -> Array[Char] {
  let result : Array[Char] = []
  for c in label {
    result.push(c)
  }
  result
}

///|
/// Validate hyphen restrictions
fn validate_hyphens(
  _label : String,
  chars : Array[Char],
) -> Unit raise IdnaError {
  // Cannot start or end with hyphen
  if chars.length() > 0 {
    if chars[0] == '-' || chars[chars.length() - 1] == '-' {
      raise InvalidHyphen
    }
  }

  // Cannot have hyphens at positions 3 and 4 (third and fourth characters)
  // This catches things like "xn--" in non-ACE context
  if chars.length() >= 4 {
    if chars[2] == '-' && chars[3] == '-' {
      raise InvalidHyphen
    }
  }
}

///|
/// Check if a character is a combining mark (General_Category = Mn, Mc, or Me)
/// This is used for the IDNA "no leading combining mark" validation (V5/V6)
fn is_combining_mark(c : Char) -> Bool {
  @ucd.is_mark(c)
}

///|
/// Check if an ASCII character is an LDH character (Letter, Digit, Hyphen)
/// This implements STD3 ASCII rules from RFC 952/1123
fn is_ldh_ascii(c : Char) -> Bool {
  // ASCII lowercase letters (a-z)
  (c >= 'a' && c <= 'z') ||
  // ASCII uppercase letters (A-Z) - should be mapped to lowercase already
  (c >= 'A' && c <= 'Z') ||
  // ASCII digits (0-9)
  (c >= '0' && c <= '9') ||
  // Hyphen-minus
  c == '-'
}

///|
/// Validate that a character has an acceptable IDNA status
fn validate_char_status(
  c : Char,
  use_std3_ascii_rules : Bool,
) -> Unit raise IdnaError {
  // STD3 ASCII rules: only LDH characters allowed in ASCII range
  // This check is separate from IdnaMappingTable.txt which marks
  // these characters as "valid" but they should be rejected under STD3
  if use_std3_ascii_rules && c.to_int() < 0x80 && !is_ldh_ascii(c) {
    raise DisallowedCodePoint(cp=c.to_int())
  }
  match @idna_data.lookup_idna_mapping(c) {
    @idna_data.Valid | @idna_data.Deviation(_) =>
      // OK
      ()
    @idna_data.Mapped(_) | @idna_data.Ignored =>
      // These should have been processed in the mapping step
      // If we see them here, something went wrong
      raise DisallowedCodePoint(cp=c.to_int())
    @idna_data.Disallowed => raise DisallowedCodePoint(cp=c.to_int())
    @idna_data.DisallowedSTD3Valid =>
      if use_std3_ascii_rules {
        raise DisallowedCodePoint(cp=c.to_int())
      }
    // Otherwise OK
    @idna_data.DisallowedSTD3Mapped(_) =>
      if use_std3_ascii_rules {
        raise DisallowedCodePoint(cp=c.to_int())
      }
    // Otherwise should have been mapped
  }
}

///|
/// Validate ContextJ rules for ZWNJ (U+200C) and ZWJ (U+200D)
fn validate_contextj(chars : Array[Char]) -> Unit raise IdnaError {
  for i, c in chars {
    if c == '\u{200C}' {
      // ZWNJ: Zero Width Non-Joiner
      if !is_valid_zwnj_context(chars, i) {
        raise InvalidJoiner
      }
    } else if c == '\u{200D}' {
      // ZWJ: Zero Width Joiner
      if !is_valid_zwj_context(chars, i) {
        raise InvalidJoiner
      }
    }
  }
}

///|
/// Check if ZWNJ is in a valid context
/// Valid if:
/// - There is a combining Virama (U+094D, etc.) before, OR
/// - Surrounded by appropriate joining characters
fn is_valid_zwnj_context(chars : Array[Char], pos : Int) -> Bool {
  // Rule 1: After a Virama (e.g., for Indic scripts)
  if pos > 0 && is_virama(chars[pos - 1]) {
    return true
  }

  // Rule 2: Between joining characters
  // Look for L or D type before, and R or D type after
  if has_left_joining_before(chars, pos) && has_right_joining_after(chars, pos) {
    return true
  }
  false
}

///|
/// Check if ZWJ is in a valid context
/// Valid if preceded by a Virama
fn is_valid_zwj_context(chars : Array[Char], pos : Int) -> Bool {
  // ZWJ is valid if preceded by a combining Virama
  pos > 0 && is_virama(chars[pos - 1])
}

///|
/// Check if a character is a Virama (combining class 9)
fn is_virama(c : Char) -> Bool {
  @ucd.lookup_ccc(c) == 9
}

///|
/// Check if there's a left-joining or dual-joining character before position
fn has_left_joining_before(chars : Array[Char], pos : Int) -> Bool {
  for i = pos - 1; i >= 0; i = i - 1 {
    let jt = @idna_data.lookup_joining_type(chars[i])
    match jt {
      @idna_data.LeftJoining | @idna_data.DualJoining => return true
      @idna_data.Transparent => continue
      _ => return false
    }
  }
  false
}

///|
/// Check if there's a right-joining or dual-joining character after position
fn has_right_joining_after(chars : Array[Char], pos : Int) -> Bool {
  for i = pos + 1; i < chars.length(); i = i + 1 {
    let jt = @idna_data.lookup_joining_type(chars[i])
    match jt {
      @idna_data.RightJoining | @idna_data.DualJoining => return true
      @idna_data.Transparent => continue
      _ => return false
    }
  }
  false
}

///|
/// Validate Bidi rules (RFC 5893) for a single label
/// Note: For proper RFC 5893 compliance, use validate_domain_bidi() at domain level
fn validate_bidi(chars : Array[Char]) -> Unit raise IdnaError {
  if chars.length() == 0 {
    return
  }

  // Determine if this is an RTL label
  let is_rtl_label = is_rtl_label_check(chars)
  if is_rtl_label {
    validate_rtl_label(chars)
  } else {
    validate_ltr_label(chars)
  }
}

///|
/// Validate Bidi rules (RFC 5893) for an entire domain
/// RFC 5893 requires domain-level checking: when ANY label is RTL,
/// all labels must follow stricter rules.
fn validate_domain_bidi(labels : Array[String]) -> Unit raise IdnaError {
  // Check if domain contains any RTL labels
  let mut has_rtl_label = false
  for label in labels {
    if label.length() > 0 {
      let chars = label_to_chars(label)
      if is_rtl_label_check(chars) {
        has_rtl_label = true
        break
      }
    }
  }
  if !has_rtl_label {
    // Not a Bidi domain name - no Bidi validation needed per UTS #46
    // "If CheckBidi, and if the domain name is a Bidi domain name, then..."
    return
  }

  // RTL domain: stricter rules apply to all labels
  for label in labels {
    if label.length() == 0 {
      continue
    }
    let chars = label_to_chars(label)
    let is_rtl = is_rtl_label_check(chars)
    if is_rtl {
      validate_rtl_label(chars)
    } else {
      // LTR label in RTL domain - must start with L (stricter rule)
      validate_ltr_label_in_rtl_domain(chars)
    }
  }
}

///|
/// Validate LTR label that appears in an RTL domain
/// Stricter than regular LTR validation: first char must be L (not EN/ON/etc.)
fn validate_ltr_label_in_rtl_domain(
  chars : Array[Char],
) -> Unit raise IdnaError {
  if chars.length() == 0 {
    return
  }

  // In RTL domain, LTR labels must start with L (not EN, ES, etc.)
  // This is stricter than pure LTR domain rules
  let first_bc = @bidi.bidi_class(chars[0])
  match first_bc {
    @bidi.L => ()
    _ => raise InvalidBidi // B1 - first char must be L in RTL domain context
  }

  // Track last non-NSM for ending check
  let mut last_non_nsm_bc = first_bc

  // Same allowed classes as regular LTR, but no R/AL/AN
  for c in chars {
    let bc = @bidi.bidi_class(c)
    match bc {
      @bidi.L
      | @bidi.EN
      | @bidi.ES
      | @bidi.CS
      | @bidi.ET
      | @bidi.ON
      | @bidi.BN
      | @bidi.NSM => if !(bc is @bidi.NSM) { last_non_nsm_bc = bc }
      @bidi.R | @bidi.AL | @bidi.AN => raise InvalidBidi
      _ => ()
    }
  }

  // Rule 6 (B6): Last non-NSM character must be L or EN
  match last_non_nsm_bc {
    @bidi.L | @bidi.EN => ()
    _ => raise InvalidBidi // B6 - ending violation
  }
}

///|
/// Check if label is RTL (contains R, AL, or AN characters)
fn is_rtl_label_check(chars : Array[Char]) -> Bool {
  for c in chars {
    let bc = @bidi.bidi_class(c)
    match bc {
      @bidi.R | @bidi.AL | @bidi.AN => return true
      _ => continue
    }
  }
  false
}

///|
/// Validate an RTL label according to RFC 5893
fn validate_rtl_label(chars : Array[Char]) -> Unit raise IdnaError {
  // Rule 1 (B1): First character must be R or AL
  let first_bc = @bidi.bidi_class(chars[0])
  match first_bc {
    @bidi.R | @bidi.AL => ()
    _ => raise InvalidBidi
  }

  // Track last non-NSM character for ending check (B3)
  // and whether EN/AN appear (B4)
  let mut last_non_nsm_bc : @bidi.BidiClass = first_bc
  let mut has_en = false
  let mut has_an = false
  for c in chars {
    let bc = @bidi.bidi_class(c)
    match bc {
      @bidi.R
      | @bidi.AL
      | @bidi.AN
      | @bidi.EN
      | @bidi.ES
      | @bidi.CS
      | @bidi.ET
      | @bidi.ON
      | @bidi.BN
      | @bidi.NSM => {
        // Rule 2 (B2): Allowed in RTL label
        if !(bc is @bidi.NSM) {
          last_non_nsm_bc = bc
        }
        if bc is @bidi.EN {
          has_en = true
        }
        if bc is @bidi.AN {
          has_an = true
        }
      }
      @bidi.L => raise InvalidBidi // B2 - L not allowed in RTL label
      _ => ()
    }
  }

  // Rule 3 (B3): Last non-NSM character must be R, AL, EN, or AN
  match last_non_nsm_bc {
    @bidi.R | @bidi.AL | @bidi.EN | @bidi.AN => ()
    _ => raise InvalidBidi
  }

  // Rule 4 (B4): EN and AN cannot both appear
  if has_en && has_an {
    raise InvalidBidi
  }
}

///|
/// Validate an LTR label according to RFC 5893
fn validate_ltr_label(chars : Array[Char]) -> Unit raise IdnaError {
  if chars.length() == 0 {
    return
  }

  // Rule 5 (B5): First character must be L
  // (Relaxed for pure ASCII/neutral labels in pure LTR domains)
  let first_bc = @bidi.bidi_class(chars[0])
  match first_bc {
    @bidi.L => ()
    // Allow pure ASCII/neutral labels (existing behavior for pure LTR domains)
    @bidi.EN
    | @bidi.ES
    | @bidi.CS
    | @bidi.ET
    | @bidi.ON
    | @bidi.BN
    | @bidi.NSM => ()
    _ => raise InvalidBidi
  }

  // Track last non-NSM character for ending check (B6)
  let mut last_non_nsm_bc = first_bc

  // Rule 5: Only certain Bidi classes allowed in LTR label
  for c in chars {
    let bc = @bidi.bidi_class(c)
    match bc {
      @bidi.L
      | @bidi.EN
      | @bidi.ES
      | @bidi.CS
      | @bidi.ET
      | @bidi.ON
      | @bidi.BN
      | @bidi.NSM =>
        // Allowed in LTR label
        if !(bc is @bidi.NSM) {
          last_non_nsm_bc = bc
        }
      @bidi.R | @bidi.AL | @bidi.AN => raise InvalidBidi // RTL chars not allowed
      _ => ()
    }
  }

  // Rule 6 (B6): Last non-NSM character must be L or EN
  match last_non_nsm_bc {
    @bidi.L | @bidi.EN => ()
    _ => raise InvalidBidi // B6 - ending violation
  }
}