//! String case-conversion helpers.
//!
//! All functions are pure and allocation-free when the input already matches
//! the target convention.

/// Convert a string to `camelCase`.
///
/// Words are split on `_`, `-`, spaces, and existing case boundaries.
pub fn to_camel_case(s: &str) -> String {
    let words = split_words(s);
    words
        .iter()
        .enumerate()
        .map(|(i, w)| {
            if i == 0 {
                w.to_lowercase()
            } else {
                capitalize(w)
            }
        })
        .collect()
}

/// Convert a string to `PascalCase`.
pub fn to_pascal_case(s: &str) -> String {
    split_words(s).iter().map(|w| capitalize(w)).collect()
}

/// Convert a string to `snake_case`.
pub fn to_snake_case(s: &str) -> String {
    split_words(s)
        .iter()
        .map(|w| w.to_lowercase())
        .collect::<Vec<_>>()
        .join("_")
}

/// Convert a string to `kebab-case`.
pub fn to_kebab_case(s: &str) -> String {
    split_words(s)
        .iter()
        .map(|w| w.to_lowercase())
        .collect::<Vec<_>>()
        .join("-")
}

/// Convert a string to `SCREAMING_SNAKE_CASE`.
pub fn to_screaming_snake(s: &str) -> String {
    split_words(s)
        .iter()
        .map(|w| w.to_uppercase())
        .collect::<Vec<_>>()
        .join("_")
}

/// Convert a string to `dot.case`.
pub fn to_dot_case(s: &str) -> String {
    split_words(s)
        .iter()
        .map(|w| w.to_lowercase())
        .collect::<Vec<_>>()
        .join(".")
}

/// Convert a string to `Title Case`.
pub fn to_title_case(s: &str) -> String {
    split_words(s)
        .iter()
        .map(|w| capitalize(w))
        .collect::<Vec<_>>()
        .join(" ")
}

/// Convert a string to `Title Case` (alias for to_title_case).
pub fn to_headline(s: &str) -> String {
    to_title_case(s)
}

/// Convert a string to `Sentence case`.
pub fn to_sentence_case(s: &str) -> String {
    let words = split_words(s);
    if words.is_empty() {
        return String::new();
    }
    let mut result = words[0].to_lowercase();
    for word in &words[1..] {
        result.push(' ');
        result.push_str(&word.to_lowercase());
    }
    result
}

/// Convert a string to `noCase` (strips all casing).
pub fn to_no_case(s: &str) -> String {
    split_words(s)
        .iter()
        .map(|w| w.to_lowercase())
        .collect::<Vec<_>>()
        .join(" ")
}

/// Convert a string to uppercase.
pub fn to_upper(s: &str) -> String {
    s.to_uppercase()
}

/// Convert a string to lowercase.
pub fn to_lower(s: &str) -> String {
    s.to_lowercase()
}

/// Convert first character of string to uppercase.
pub fn ucfirst(s: &str) -> String {
    let mut chars = s.chars();
    match chars.next() {
        None => String::new(),
        Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
    }
}

/// Convert first character of string to lowercase.
pub fn lcfirst(s: &str) -> String {
    let mut chars = s.chars();
    match chars.next() {
        None => String::new(),
        Some(first) => first.to_lowercase().collect::<String>() + chars.as_str(),
    }
}

/// Invert the case of a string.
pub fn invert_case(s: &str) -> String {
    s.chars()
        .map(|c| {
            if c.is_uppercase() {
                c.to_lowercase().collect()
            } else if c.is_lowercase() {
                c.to_uppercase().collect()
            } else {
                c.to_string()
            }
        })
        .collect()
}

/// Truncate `s` to at most `max` characters, appending `"..."` if truncation
/// occurs.
///
/// Truncation is always on a valid UTF-8 character boundary, so the result may
/// be shorter than `max` for multi-byte inputs.
///
/// ```rust
/// use rok_utils::string::truncate;
/// assert_eq!(truncate("hello world", 5), "he...");
/// assert_eq!(truncate("hi", 10), "hi");
/// ```
pub fn truncate(s: &str, max: usize) -> String {
    if s.len() <= max {
        return s.to_string();
    }
    // Reserve 3 chars for "..."
    let cut = max.saturating_sub(3);
    let mut end = cut;
    while end > 0 && !s.is_char_boundary(end) {
        end -= 1;
    }
    format!("{}...", &s[..end])
}

/// Return a naive English plural of `word`.
///
/// Handles common English inflection rules:
/// * words ending in `s`, `x`, `z`, `ch`, `sh` → `+es`
/// * words ending in a consonant + `y` → `-y` + `ies`
/// * words ending in `f` / `fe` → `-f[e]` + `ves`
/// * words ending in `us` → `-us` + `i`
/// * everything else → `+s`
///
/// ```rust
/// use rok_utils::string::pluralize;
/// assert_eq!(pluralize("user"), "users");
/// assert_eq!(pluralize("box"), "boxes");
/// assert_eq!(pluralize("category"), "categories");
/// assert_eq!(pluralize("leaf"), "leaves");
/// ```
pub fn pluralize(word: &str) -> String {
    if word.is_empty() {
        return word.to_string();
    }
    let lower = word.to_lowercase();
    if lower.ends_with("sis") {
        return format!("{}es", &word[..word.len() - 3]);
    }
    if lower.ends_with("fe") {
        return format!("{}ves", &word[..word.len() - 2]);
    }
    if lower.ends_with('f') && !lower.ends_with("ff") {
        return format!("{}ves", &word[..word.len() - 1]);
    }
    if lower.ends_with("us") {
        return format!("{}i", &word[..word.len() - 2]);
    }
    if lower.ends_with("ch") || lower.ends_with("sh") {
        return format!("{}es", word);
    }
    if lower.ends_with('s') || lower.ends_with('x') || lower.ends_with('z') {
        return format!("{}es", word);
    }
    if lower.ends_with('y') {
        let prev = lower.chars().rev().nth(1);
        if !matches!(prev, Some('a' | 'e' | 'i' | 'o' | 'u')) {
            return format!("{}ies", &word[..word.len() - 1]);
        }
    }
    format!("{}s", word)
}

/// Convert a string to a URL-safe slug with the given separator.
pub fn slug(s: &str, separator: char) -> String {
    let words = split_words(s);
    words
        .iter()
        .map(|w| w.to_lowercase())
        .collect::<Vec<_>>()
        .join(&separator.to_string())
}

/// Collapse consecutive whitespace to single spaces and trim edges.
pub fn squish(s: &str) -> String {
    let mut result = String::new();
    let mut last_was_space = false;
    let mut started = false;

    for c in s.chars() {
        if c.is_whitespace() {
            if !started {
                continue;
            }
            if !last_was_space {
                result.push(' ');
                last_was_space = true;
            }
        } else {
            result.push(c);
            last_was_space = false;
            started = true;
        }
    }
    if result.ends_with(' ') {
        result.pop();
    }
    result
}

/// Mask characters from `index` onward with the given character.
pub fn mask(s: &str, mask_char: char, index: usize) -> String {
    if index >= s.len() {
        return s.chars().map(|_| mask_char).collect();
    }
    let mut result = s[..index].to_string();
    result.extend(s.chars().skip(index).map(|_| mask_char));
    result
}

/// Wrap a string with prefix and suffix.
pub fn wrap(s: &str, before: &str, after: &str) -> String {
    format!("{}{}{}", before, s, after)
}

/// Remove prefix and suffix if present.
pub fn unwrap(s: &str, before: &str, after: &str) -> String {
    if s.starts_with(before) && s.ends_with(after) {
        let start = before.len();
        let end = s.len() - after.len();
        s[start..end].to_string()
    } else {
        s.to_string()
    }
}

/// Pad string to length on the left.
pub fn pad_left(s: &str, length: usize, pad: char) -> String {
    if s.len() >= length {
        return s.to_string();
    }
    let count = length - s.len();
    let padding: String = pad.to_string().repeat(count);
    format!("{}{}", padding, s)
}

/// Pad string to length on the right.
pub fn pad_right(s: &str, length: usize, pad: char) -> String {
    if s.len() >= length {
        return s.to_string();
    }
    let count = length - s.len();
    let padding: String = pad.to_string().repeat(count);
    format!("{}{}", s, padding)
}

/// Pad string to length on both sides.
pub fn pad_both(s: &str, length: usize, pad: char) -> String {
    if s.len() >= length {
        return s.to_string();
    }
    let count = length - s.len();
    let left = count / 2;
    let right = count - left;
    let left_pad: String = pad.to_string().repeat(left);
    let right_pad: String = pad.to_string().repeat(right);
    format!("{}{}{}", left_pad, s, right_pad)
}

/// Repeat a string `times` times.
pub fn repeat(s: &str, times: usize) -> String {
    s.repeat(times)
}

/// Reverse a string.
pub fn reverse(s: &str) -> String {
    s.chars().rev().collect()
}

/// Replace first occurrence of `from` with `to`.
pub fn replace_first(s: &str, from: &str, to: &str) -> String {
    if let Some(pos) = s.find(from) {
        let mut result = s[..pos].to_string();
        result.push_str(to);
        result.push_str(&s[pos + from.len()..]);
        result
    } else {
        s.to_string()
    }
}

/// Replace last occurrence of `from` with `to`.
pub fn replace_last(s: &str, from: &str, to: &str) -> String {
    if let Some(pos) = s.rfind(from) {
        let mut result = s[..pos].to_string();
        result.push_str(to);
        result.push_str(&s[pos + from.len()..]);
        result
    } else {
        s.to_string()
    }
}

/// Ensure string ends with the given value.
pub fn finish(s: &str, cap: &str) -> String {
    if s.ends_with(cap) {
        s.to_string()
    } else {
        format!("{}{}", s, cap)
    }
}

/// Ensure string starts with the given value.
pub fn ensure_start(s: &str, prefix: &str) -> String {
    if s.starts_with(prefix) {
        s.to_string()
    } else {
        format!("{}{}", prefix, s)
    }
}

// ── internal helpers ─────────────────────────────────────────────────────────

/// Split a string into lowercase words by:
///   - `_` / `-` / space separators
///   - camelCase / PascalCase boundaries
fn split_words(s: &str) -> Vec<String> {
    let mut words: Vec<String> = Vec::new();
    let mut current = String::new();

    let chars: Vec<char> = s.chars().collect();
    for (i, &c) in chars.iter().enumerate() {
        if c == '_' || c == '-' || c == ' ' {
            if !current.is_empty() {
                words.push(current.clone());
                current.clear();
            }
        } else if c.is_uppercase() {
            // Start a new word on uppercase unless it follows another uppercase
            // (acronym) or is the first character.
            let prev_lower = i > 0 && chars[i - 1].is_lowercase();
            let next_lower = chars
                .get(i + 1)
                .map(|ch| ch.is_lowercase())
                .unwrap_or(false);
            let acronym_end = i > 0 && chars[i - 1].is_uppercase() && next_lower;
            if !current.is_empty() && (prev_lower || acronym_end) {
                words.push(current.clone());
                current.clear();
            }
            current.push(c);
        } else {
            current.push(c);
        }
    }
    if !current.is_empty() {
        words.push(current);
    }
    words
}

fn capitalize(s: &str) -> String {
    let mut chars = s.chars();
    match chars.next() {
        None => String::new(),
        Some(first) => first.to_uppercase().collect::<String>() + &chars.as_str().to_lowercase(),
    }
}

/// Check if string is empty (after trimming).
pub fn is_empty(s: &str) -> bool {
    s.trim().is_empty()
}

/// Check if string contains only ASCII characters.
pub fn is_ascii(s: &str) -> bool {
    s.is_ascii()
}

/// Check if string is a valid JSON string.
#[cfg(feature = "json")]
pub fn is_json(s: &str) -> bool {
    serde_json::from_str::<serde_json::Value>(s).is_ok()
}

/// Check if string is a valid URL.
pub fn is_url(s: &str) -> bool {
    s.starts_with("http://") || s.starts_with("https://") || s.starts_with("ftp://")
}

/// Check if string is a valid UUID.
#[cfg(feature = "ids")]
pub fn is_uuid(s: &str) -> bool {
    uuid::Uuid::parse_str(s).is_ok()
}

/// Check if string is a valid ULID.
pub fn is_ulid(s: &str) -> bool {
    s.len() == 26 && s.chars().all(|c| c.is_ascii_alphanumeric())
}

/// Check if string contains only alphanumeric characters.
pub fn is_alphanumeric(s: &str) -> bool {
    !s.is_empty() && s.chars().all(|c| c.is_alphanumeric())
}

/// Return the character count (not byte count).
pub fn length(s: &str) -> usize {
    s.chars().count()
}

/// Return the word count.
pub fn word_count(s: &str) -> usize {
    s.split_whitespace().count()
}

/// Get character at index.
pub fn char_at(s: &str, index: usize) -> Option<char> {
    s.chars().nth(index)
}

/// Find position of needle in string.
pub fn position(s: &str, needle: &str) -> Option<usize> {
    s.find(needle)
}

/// Count occurrences of substring.
pub fn substr_count(s: &str, needle: &str) -> usize {
    s.matches(needle).count()
}

/// Check if string starts with needle.
pub fn starts_with(s: &str, needle: &str) -> bool {
    s.starts_with(needle)
}

/// Check if string ends with needle.
pub fn ends_with(s: &str, needle: &str) -> bool {
    s.ends_with(needle)
}

/// Check if string contains needle.
pub fn contains(s: &str, needle: &str) -> bool {
    s.contains(needle)
}

/// Check if string contains all needles.
pub fn contains_all(s: &str, needles: &[&str]) -> bool {
    needles.iter().all(|n| s.contains(n))
}

/// Check if string does NOT contain needle.
pub fn doesnt_contain(s: &str, needle: &str) -> bool {
    !s.contains(needle)
}

/// Convert nanoseconds to human-readable duration string.
pub fn pretty_duration(nanos: u64) -> String {
    if nanos < 1_000 {
        return format!("{}ns", nanos);
    }
    let micros = nanos / 1_000;
    if micros < 1_000 {
        return format!("{}μs", micros);
    }
    let millis = micros / 1_000;
    if millis < 1_000 {
        return format!("{}ms", millis);
    }
    let seconds = millis / 1_000;
    if seconds < 60 {
        return format!("{}s", seconds);
    }
    let minutes = seconds / 60;
    if minutes < 60 {
        return format!("{}m", minutes);
    }
    let hours = minutes / 60;
    format!("{}h", hours)
}

/// Generate a cryptographically secure random string of given length.
/// Uses URL-safe base64 encoding for the result.
#[cfg(feature = "random")]
pub fn random(length: usize) -> String {
    use rand::RngCore;
    let mut bytes = vec![0u8; length];
    rand::thread_rng().fill_bytes(&mut bytes);
    use base64::Engine;
    base64::engine::general_purpose::URL_SAFE.encode(&bytes)[..length].to_string()
}

/// Generate a secure password with letters, digits, and optionally symbols.
#[cfg(feature = "random")]
pub fn password(length: usize, symbols: bool) -> String {
    use rand::Rng;
    const LETTERS: &[u8] = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
    const DIGITS: &[u8] = b"0123456789";
    const SYMBOLS: &[u8] = b"!@#$%^&*()_+-=[]{}|;:,.<>?";

    let mut rng = rand::thread_rng();
    let mut chars = Vec::new();
    chars.extend_from_slice(LETTERS);
    chars.extend_from_slice(DIGITS);
    if symbols {
        chars.extend_from_slice(SYMBOLS);
    }

    (0..length)
        .map(|_| chars[rng.gen_range(0..chars.len())] as char)
        .collect()
}

/// Convert string to base64 encoding.
pub fn to_base64(s: &str) -> String {
    use base64::Engine;
    base64::engine::general_purpose::STANDARD.encode(s.as_bytes())
}

/// Escape HTML special characters.
#[cfg(feature = "json")]
pub fn escape_html(s: &str) -> String {
    s.replace('&', "&amp;")
        .replace('<', "&lt;")
        .replace('>', "&gt;")
        .replace('"', "&quot;")
        .replace('\'', "&#39;")
}

// ── tests ────────────────────────────────────────────────────────────────────

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn snake_to_camel() {
        assert_eq!(to_camel_case("hello_world"), "helloWorld");
        assert_eq!(to_camel_case("foo_bar_baz"), "fooBarBaz");
    }

    #[test]
    fn snake_to_pascal() {
        assert_eq!(to_pascal_case("hello_world"), "HelloWorld");
    }

    #[test]
    fn camel_to_snake() {
        assert_eq!(to_snake_case("helloWorld"), "hello_world");
        assert_eq!(to_snake_case("FooBarBaz"), "foo_bar_baz");
    }

    #[test]
    fn camel_to_kebab() {
        assert_eq!(to_kebab_case("helloWorld"), "hello-world");
    }

    #[test]
    fn screaming() {
        assert_eq!(to_screaming_snake("hello_world"), "HELLO_WORLD");
    }

    #[test]
    fn passthrough_unchanged() {
        assert_eq!(to_snake_case("already_snake"), "already_snake");
        assert_eq!(to_pascal_case("AlreadyPascal"), "AlreadyPascal");
    }

    #[test]
    fn truncate_short_string_unchanged() {
        assert_eq!(truncate("hi", 10), "hi");
        assert_eq!(truncate("hello", 5), "hello");
    }

    #[test]
    fn truncate_long_string() {
        assert_eq!(truncate("hello world", 5), "he...");
        assert_eq!(truncate("abcdefgh", 6), "abc...");
    }

    #[test]
    fn pluralize_regular() {
        assert_eq!(pluralize("user"), "users");
        assert_eq!(pluralize("item"), "items");
    }

    #[test]
    fn pluralize_sibilant() {
        assert_eq!(pluralize("box"), "boxes");
        assert_eq!(pluralize("church"), "churches");
        assert_eq!(pluralize("dish"), "dishes");
        assert_eq!(pluralize("buzz"), "buzzes");
    }

    #[test]
    fn pluralize_y_ending() {
        assert_eq!(pluralize("category"), "categories");
        assert_eq!(pluralize("city"), "cities");
        assert_eq!(pluralize("day"), "days"); // vowel + y → just +s
    }

    #[test]
    fn pluralize_f_ending() {
        assert_eq!(pluralize("leaf"), "leaves");
        assert_eq!(pluralize("knife"), "knives");
    }

    #[test]
    fn pluralize_us_ending() {
        assert_eq!(pluralize("cactus"), "cacti");
        assert_eq!(pluralize("focus"), "foci");
    }
}
