//! Cryptographically secure random secret generation.
//!
//! Uses [`OsRng`] (OS entropy source) for all output so secrets
//! are safe for use as passwords, API keys, and other credentials.

use rand::{rngs::OsRng, RngCore};

const ALNUM: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
const ALPHA: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
const NUMERIC: &[u8] = b"0123456789";
const HEX: &[u8] = b"0123456789abcdef";
const SYMBOL: &[u8] =
    b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()-_=+[]{}|;:,.<>?";

/// Characters that are visually ambiguous (easily confused when read aloud or transcribed).
const AMBIGUOUS: &[u8] = b"0Ol1I";

fn resolve_charset(charset: &str) -> &'static [u8] {
    match charset {
        "alpha" => ALPHA,
        "numeric" => NUMERIC,
        "hex" => HEX,
        "symbol" => SYMBOL,
        _ => ALNUM,
    }
}

/// Generate a random string of `length` characters drawn from `charset`.
///
/// `charset` is one of: `"alnum"` (default), `"alpha"`, `"numeric"`, `"hex"`, `"symbol"`.
/// Unrecognised values fall back to `"alnum"`.
pub fn generate(length: usize, charset: &str) -> String {
    let chars = resolve_charset(charset);
    let mut buf = [0u8; 4];
    (0..length)
        .map(|_| {
            OsRng.fill_bytes(&mut buf);
            let idx = u32::from_le_bytes(buf) as usize % chars.len();
            chars[idx] as char
        })
        .collect()
}

/// Generate a random string with visually ambiguous characters removed from the pool.
///
/// Ambiguous characters excluded: `0`, `O`, `l`, `1`, `I`.
/// This is useful when the secret will be read aloud, displayed on-screen, or transcribed manually.
pub fn generate_unambiguous(length: usize, charset: &str) -> String {
    let chars: Vec<u8> = resolve_charset(charset)
        .iter()
        .copied()
        .filter(|c| !AMBIGUOUS.contains(c))
        .collect();
    if chars.is_empty() {
        // Charset has only ambiguous chars (unlikely but guard against panic).
        return generate(length, charset);
    }
    let mut buf = [0u8; 4];
    (0..length)
        .map(|_| {
            OsRng.fill_bytes(&mut buf);
            let idx = u32::from_le_bytes(buf) as usize % chars.len();
            chars[idx] as char
        })
        .collect()
}

/// Return the number of unique characters in the effective character pool for `charset`,
/// optionally with ambiguous characters removed.
pub fn effective_pool_size(charset: &str, exclude_ambiguous: bool) -> usize {
    let chars = resolve_charset(charset);
    if exclude_ambiguous {
        chars.iter().filter(|c| !AMBIGUOUS.contains(c)).count()
    } else {
        chars.len()
    }
}

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

    #[test]
    fn length_is_exact() {
        assert_eq!(generate(32, "alnum").len(), 32);
        assert_eq!(generate(0, "alnum").len(), 0);
        assert_eq!(generate(128, "hex").len(), 128);
    }

    #[test]
    fn charset_alnum_contains_only_valid_chars() {
        let s = generate(1000, "alnum");
        assert!(
            s.chars().all(|c| c.is_ascii_alphanumeric()),
            "unexpected char in alnum output"
        );
    }

    #[test]
    fn charset_numeric_is_all_digits() {
        let s = generate(64, "numeric");
        assert!(s.chars().all(|c| c.is_ascii_digit()));
    }

    #[test]
    fn charset_hex_is_lowercase() {
        let s = generate(64, "hex");
        assert!(s.chars().all(|c| "0123456789abcdef".contains(c)));
    }

    #[test]
    fn unknown_charset_falls_back_to_alnum() {
        let s = generate(64, "garbage");
        assert!(s.chars().all(|c| c.is_ascii_alphanumeric()));
    }
}
