///|
fn lower(text : String) -> String {
  text.to_lower()
}

///|
fn has_text(text : String) -> Bool {
  !text.trim().is_empty()
}

///|
fn contains_ci(text : String, needle : String) -> Bool {
  lower(text).contains(lower(needle))
}

///|
fn json_escape(text : String) -> String {
  text
  .replace_all(old="\\", new="\\\\")
  .replace_all(old="\"", new="\\\"")
  .replace_all(old="\n", new="\\n")
}

///|
fn clamp(value : Int, low : Int, high : Int) -> Int {
  if value < low {
    low
  } else if value > high {
    high
  } else {
    value
  }
}

///|
fn severity_text(severity : Severity) -> String {
  match severity {
    Pass => "pass"
    Warn => "warn"
    Fail => "fail"
  }
}

///|
fn region_text(region : Region) -> String {
  match region {
    NA => "na"
    EU => "eu"
    APAC => "apac"
    CN => "cn"
    LATAM => "latam"
    MEA => "mea"
    OtherRegion => "other"
  }
}

///|
fn tier_text(tier : UserTier) -> String {
  match tier {
    Free => "free"
    Pro => "pro"
    Enterprise => "enterprise"
    Staff => "staff"
    Internal => "internal"
  }
}

///|
pub fn mode_text(mode : RolloutMode) -> String {
  match mode {
    Shadow => "shadow"
    DryRun => "dry-run"
    Live => "live"
    KillSwitch => "kill-switch"
  }
}

///|
fn decision_text(decision : GateDecision) -> String {
  match decision {
    Advance => "advance"
    Hold => "hold"
    Rollback => "rollback"
    Abort => "abort"
  }
}

///|
fn kind_text(kind : ValidationKind) -> String {
  match kind {
    PlanShape => "plan"
    SegmentShape => "segment"
    RiskBudgetShape => "budget"
    MetricGate => "metrics"
    BlastRadiusGate => "blast-radius"
    ScheduleGate => "schedule"
    IdentityGate => "identity"
  }
}

///|
fn percent_text(bp : Int) -> String {
  let whole = bp / 100
  let frac = bp % 100
  if frac == 0 {
    whole.to_string() + "%"
  } else if frac < 10 {
    whole.to_string() + ".0" + frac.to_string() + "%"
  } else {
    whole.to_string() + "." + frac.to_string() + "%"
  }
}

///|
fn parse_digits(text : String) -> Int? {
  let trimmed = text.trim().to_owned()
  if trimmed.is_empty() {
    return None
  }
  let mut value = 0
  for ch in trimmed {
    let code = ch.to_int()
    if code < 48 || code > 57 {
      return None
    }
    value = value * 10 + code - 48
  }
  Some(value)
}

///|
fn version_score(version : String) -> Int {
  let parts = version.split(".").to_array()
  let major = if parts.length() > 0 {
    match parse_digits(parts[0].to_owned()) {
      Some(v) => v
      None => 0
    }
  } else {
    0
  }
  let minor = if parts.length() > 1 {
    match parse_digits(parts[1].to_owned()) {
      Some(v) => v
      None => 0
    }
  } else {
    0
  }
  let patch = if parts.length() > 2 {
    match parse_digits(parts[2].to_owned()) {
      Some(v) => v
      None => 0
    }
  } else {
    0
  }
  major * 1000000 + minor * 1000 + patch
}