///|
/// IDNA (Internationalized Domain Names in Applications) processing
/// Implements UTS #46: Unicode IDNA Compatibility Processing
/// https://www.unicode.org/reports/tr46/

///|
/// Error types for IDNA processing
pub(all) suberror IdnaError {
  PunycodeError(@punycode.PunycodeError)
  DisallowedCodePoint(cp~ : Int)
  InvalidLabel(String)
  InvalidHyphen
  LabelTooLong
  DomainTooLong
  EmptyLabel
  InvalidBidi
  InvalidJoiner
  InvalidAcePrefix
  LeadingCombiningMark
}

///|
pub impl Show for IdnaError with fn output(self, logger) {
  match self {
    PunycodeError(error) => {
      logger.write_string("PunycodeError(")
      error.output(logger)
      logger.write_string(")")
    }
    DisallowedCodePoint(cp~) => {
      logger.write_string("DisallowedCodePoint(cp=")
      cp.output(logger)
      logger.write_string(")")
    }
    InvalidLabel(label) => {
      logger.write_string("InvalidLabel(")
      label.output(logger)
      logger.write_string(")")
    }
    InvalidHyphen => logger.write_string("InvalidHyphen")
    LabelTooLong => logger.write_string("LabelTooLong")
    DomainTooLong => logger.write_string("DomainTooLong")
    EmptyLabel => logger.write_string("EmptyLabel")
    InvalidBidi => logger.write_string("InvalidBidi")
    InvalidJoiner => logger.write_string("InvalidJoiner")
    InvalidAcePrefix => logger.write_string("InvalidAcePrefix")
    LeadingCombiningMark => logger.write_string("LeadingCombiningMark")
  }
}

///|
/// ACE prefix for Punycode labels
let ace_prefix : String = "xn--"

///|
/// Convert a domain name to ASCII (for DNS lookup)
/// This is the main entry point for IDNA ToASCII operation
pub fn to_ascii(
  domain : String,
  use_std3_ascii_rules? : Bool = true,
  check_hyphens? : Bool = true,
  check_bidi? : Bool = true,
  check_joiners? : Bool = true,
  verify_dns_length? : Bool = true,
) -> String raise IdnaError {
  // Map and normalize before splitting. Labels are validated below, after
  // existing A-labels have been decoded to their Unicode form.
  let mapped = map_domain(domain, use_std3_ascii_rules~)
  let processed = @normalization.nfc(mapped)

  // Split into labels
  let labels = split_labels(processed)
  let label_count = labels.length()
  // A valid trailing dot requires at least 2 labels (e.g., "example.com." -> ["example", "com", ""])
  // A single empty label (from empty domain like "") is NOT a trailing dot
  let has_trailing_dot = label_count > 1 && labels[label_count - 1] == ""

  // For domain-level bidi validation, we need Unicode content of all labels
  // This requires decoding ACE labels first
  let unicode_labels : Array[String] = []
  let ascii_labels : Array[String] = []
  for i, label in labels {
    // Handle trailing empty label
    if i == label_count - 1 && has_trailing_dot {
      unicode_labels.push("")
      ascii_labels.push("")
      continue
    }
    if label.length() == 0 {
      // Empty label that's not a trailing dot - this is invalid
      // (could be from IDNA mapping like soft hyphen -> "", or ".." in domain)
      raise EmptyLabel
    }

    // Process label to get both Unicode and ASCII forms
    let (unicode_label, ascii_label) = process_label_for_ascii(
      label,
      use_std3_ascii_rules~,
      check_hyphens~,
      check_joiners~,
    )
    unicode_labels.push(unicode_label)
    ascii_labels.push(ascii_label)
  }

  // Domain-level bidi validation on Unicode labels
  if check_bidi {
    let labels_for_bidi = if has_trailing_dot {
      unicode_labels.iter().take(label_count - 1).collect()
    } else {
      unicode_labels
    }
    validate_domain_bidi(labels_for_bidi)
  }
  let result = ascii_labels.join(".")

  // Verify DNS length constraints
  if verify_dns_length {
    verify_dns_length_constraints(result, ascii_labels)
  }
  result
}

///|
/// Process a single label and return both Unicode and ASCII forms
/// Used by to_ascii to collect Unicode labels for domain-level bidi validation
fn process_label_for_ascii(
  label : String,
  use_std3_ascii_rules~ : Bool,
  check_hyphens~ : Bool,
  check_joiners~ : Bool,
) -> (String, String) raise IdnaError {
  if label.length() == 0 {
    return ("", "")
  }

  // Check if label has ACE prefix (xn--)
  if has_ace_prefix(label) {
    // Decode ACE label to get Unicode content
    let decoded = decode_ace_label(label)

    // Apply mapping and normalize
    let mapped = map_label(decoded, use_std3_ascii_rules~)
    let normalized = @normalization.nfc(mapped)

    // Validate the decoded label (without bidi - done at domain level)
    validate_label(
      normalized,
      use_std3_ascii_rules~,
      check_hyphens~,
      check_bidi=false, // Bidi done at domain level
      check_joiners~,
    )

    // Re-encode for roundtrip check
    let re_encoded = encode_label_to_ascii(normalized)

    // Roundtrip check (V7 error on mismatch)
    if re_encoded != label.to_lower() {
      raise InvalidAcePrefix
    }
    return (normalized, label.to_lower())
  }

  // Validate non-ACE labels here. Raw A-labels must not be subjected to the
  // hyphen checks before decoding, because the xn-- prefix is intentional.
  validate_label(
    label,
    use_std3_ascii_rules~,
    check_hyphens~,
    check_bidi=false, // Bidi done at domain level
    check_joiners~,
  )
  let ascii_label = encode_label_to_ascii(label)
  (label, ascii_label)
}

///|
/// Convert a domain name to Unicode (for display)
/// This is the main entry point for IDNA ToUnicode operation
pub fn to_unicode(
  domain : String,
  use_std3_ascii_rules? : Bool = true,
  check_hyphens? : Bool = true,
  check_bidi? : Bool = true,
  check_joiners? : Bool = true,
) -> String raise IdnaError {
  // Split into labels first (before mapping, to handle Punycode)
  let labels = split_labels(domain)
  let label_count = labels.length()
  // A valid trailing dot requires at least 2 labels (e.g., "example.com." -> ["example", "com", ""])
  // A single empty label (from empty domain like "") is NOT a trailing dot
  let has_trailing_dot = label_count > 1 && labels[label_count - 1] == ""
  let unicode_labels : Array[String] = []
  for i, label in labels {
    // Skip processing for trailing empty label (trailing dot)
    if i == label_count - 1 && has_trailing_dot {
      unicode_labels.push("")
      continue
    }
    if label.length() == 0 {
      // Empty label that's not a trailing dot - this is invalid
      raise EmptyLabel
    }
    let unicode_label = label_to_unicode(
      label,
      use_std3_ascii_rules~,
      check_hyphens~,
      check_joiners~,
    )
    unicode_labels.push(unicode_label)
  }
  if check_bidi {
    let labels_for_bidi = if has_trailing_dot {
      unicode_labels.iter().take(label_count - 1).collect()
    } else {
      unicode_labels
    }
    validate_domain_bidi(labels_for_bidi)
  }
  unicode_labels.join(".")
}

///|
/// Encode a label to ASCII (Punycode if needed), without ACE validation
fn encode_label_to_ascii(label : String) -> String raise IdnaError {
  if is_all_ascii(label) {
    // Already ASCII, return as-is (lowercased)
    label.to_lower()
  } else {
    // Encode to Punycode
    let encoded = @punycode.encode(label.to_lower()) catch {
      error => raise PunycodeError(error)
    }
    "\{ace_prefix}\{encoded}"
  }
}

///|
/// Convert a single label from Punycode to Unicode if needed
fn label_to_unicode(
  label : String,
  use_std3_ascii_rules~ : Bool,
  check_hyphens~ : Bool,
  check_joiners~ : Bool,
) -> String raise IdnaError {
  let unicode_label = if has_ace_prefix(label) {
    decode_ace_label(label)
  } else {
    label
  }

  // Apply mapping and normalize
  let processed = map_label(unicode_label, use_std3_ascii_rules~)
  let normalized = @normalization.nfc(processed)

  // Validate the label
  validate_label(
    normalized,
    use_std3_ascii_rules~,
    check_hyphens~,
    check_bidi=false, // Bidi is validated across the complete domain
    check_joiners~,
  )
  normalized
}

///|
/// Decode an A-label and enforce the UTS #46 requirement that its decoded
/// form is non-empty and contains at least one non-ASCII code point.
fn decode_ace_label(label : String) -> String raise IdnaError {
  let punycode_part = get_substring_after_prefix(label, 4)
  let decoded = @punycode.decode(punycode_part) catch {
    error => raise PunycodeError(error)
  }
  if is_all_ascii(decoded) {
    raise InvalidAcePrefix
  }
  decoded
}

///|
fn is_all_ascii(label : String) -> Bool {
  label.all(c => c.to_int() < 0x80)
}

///|
/// Check if a label has the ACE prefix "xn--"
fn has_ace_prefix(label : String) -> Bool {
  label.to_lower().has_prefix("xn--")
}

///|
/// Get substring after a given prefix length (safe version)
fn get_substring_after_prefix(s : String, prefix_len : Int) -> String {
  let result = StringBuilder::new()
  let mut i = 0
  for c in s {
    if i >= prefix_len {
      result.write_char(c)
    }
    i = i + 1
  }
  result.to_string()
}

///|
/// Verify DNS length constraints
fn verify_dns_length_constraints(
  domain : String,
  labels : Array[String],
) -> Unit raise IdnaError {
  // Each label must be 1-63 characters
  // Empty labels are not allowed (including trailing dots in DNS)
  for label in labels {
    if label.length() == 0 {
      raise EmptyLabel
    }
    if label.length() > 63 {
      raise LabelTooLong
    }
  }

  // Total domain name must be at most 253 characters
  if domain.length() > 253 {
    raise DomainTooLong
  }
}