//! Password and passphrase generators. CSPRNG-only; rejection-sampled to
//! eliminate modulo bias. Strength rating via zxcvbn.

use bip39::{Language, Mnemonic};
use rand::distributions::{Distribution, Uniform};
use rand::rngs::OsRng;
use rand::RngCore;
use zeroize::Zeroizing;

use crate::error::{RelicarioError, Result};
use crate::settings::{Capitalization, CharClasses, GeneratorRequest, SymbolCharset};

const SAFE_SYMBOLS: &[u8] = b"!@#$%^&*-_=+";
const EXTENDED_SYMBOLS: &[u8] = b"!@#$%^&*-_=+~?.";
const LOWER: &[u8] = b"abcdefghijklmnopqrstuvwxyz";
const UPPER: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ";
const DIGITS: &[u8] = b"0123456789";

pub fn generate_password(req: &GeneratorRequest) -> Result<Zeroizing<String>> {
    match req {
        GeneratorRequest::Random { length, classes, symbol_charset } => {
            random_password(*length, classes, symbol_charset)
        }
        GeneratorRequest::Bip39 { .. } => Err(RelicarioError::Format(
            "use generate_passphrase() for BIP39 requests".into(),
        )),
    }
}

fn random_password(
    length: u32,
    classes: &CharClasses,
    symbol_charset: &SymbolCharset,
) -> Result<Zeroizing<String>> {
    if length == 0 || length > 128 {
        return Err(RelicarioError::Format("length must be 1..=128".into()));
    }
    let mut charset: Vec<u8> = Vec::new();
    if classes.lower { charset.extend_from_slice(LOWER); }
    if classes.upper { charset.extend_from_slice(UPPER); }
    if classes.digits { charset.extend_from_slice(DIGITS); }
    if classes.symbols {
        let symbols: &[u8] = match symbol_charset {
            SymbolCharset::SafeOnly => SAFE_SYMBOLS,
            SymbolCharset::Extended => EXTENDED_SYMBOLS,
            SymbolCharset::Custom(s) => {
                if !s.is_ascii() {
                    return Err(RelicarioError::Format(
                        "SymbolCharset::Custom must be ASCII-only".into(),
                    ));
                }
                s.as_bytes()
            }
        };
        charset.extend_from_slice(symbols);
    }
    if charset.is_empty() {
        return Err(RelicarioError::Format("at least one character class required".into()));
    }

    let dist = Uniform::from(0..charset.len());
    let mut rng = OsRng;
    let bytes: Vec<u8> = (0..length).map(|_| charset[dist.sample(&mut rng)]).collect();
    Ok(Zeroizing::new(String::from_utf8(bytes).expect("ascii-only charset")))
}

pub fn generate_passphrase(req: &GeneratorRequest) -> Result<Zeroizing<String>> {
    match req {
        GeneratorRequest::Bip39 { word_count, separator, capitalization } => {
            bip39_passphrase(*word_count, separator, *capitalization)
        }
        GeneratorRequest::Random { .. } => Err(RelicarioError::Format(
            "use generate_password() for Random requests".into(),
        )),
    }
}

fn bip39_passphrase(word_count: u32, separator: &str, cap: Capitalization) -> Result<Zeroizing<String>> {
    if !matches!(word_count, 3..=12) {
        return Err(RelicarioError::Format("word_count must be 3..=12".into()));
    }
    // bip39 v2 requires entropy 128–256 bits in multiples of 32 bits (4 bytes).
    // We always generate 128 bits (16 bytes) → 12 words, then take the first
    // word_count words. This gives full-entropy sourcing even for short passphrases.
    let mut entropy = Zeroizing::new([0u8; 16]);
    OsRng.fill_bytes(entropy.as_mut_slice());
    let m = Mnemonic::from_entropy_in(Language::English, entropy.as_slice())
        .map_err(|e| RelicarioError::Format(format!("bip39: {e}")))?;
    let words: Vec<String> = m.words().take(word_count as usize).map(|w| {
        match cap {
            Capitalization::Lower => w.to_ascii_lowercase(),
            Capitalization::Upper => w.to_ascii_uppercase(),
            Capitalization::FirstOfEach | Capitalization::Title => {
                let mut chars = w.chars();
                chars.next().map(|c| c.to_ascii_uppercase().to_string())
                    .unwrap_or_default() + chars.as_str()
            }
            Capitalization::Mixed => {
                w.chars().enumerate().map(|(i, c)| {
                    if i % 2 == 0 { c.to_ascii_uppercase() } else { c }
                }).collect()
            }
        }
    }).collect();
    Ok(Zeroizing::new(words.join(separator)))
}

/// Returns zxcvbn's 0-4 score (higher is stronger) and the estimated guesses.
#[derive(Debug, Clone, Copy)]
pub struct StrengthEstimate {
    pub score: u8,
    pub guesses_log10: f64,
}

pub fn rate_passphrase(p: &str) -> StrengthEstimate {
    let est = zxcvbn::zxcvbn(p, &[]);
    StrengthEstimate {
        score: est.score().into(),
        guesses_log10: est.guesses_log10(),
    }
}

/// Strength gate at vault creation (audit H3): require score >= 3.
pub fn validate_passphrase_strength(p: &str) -> Result<()> {
    let est = rate_passphrase(p);
    if est.score < 3 {
        return Err(RelicarioError::WeakPassphrase { score: est.score });
    }
    Ok(())
}

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

    #[test]
    fn bip39_default_is_5_space_separated_words() {
        let req = GeneratorRequest::Bip39 {
            word_count: 5,
            separator: " ".into(),
            capitalization: Capitalization::Lower,
        };
        let pw = generate_passphrase(&req).unwrap();
        assert_eq!(pw.split(' ').count(), 5);
    }

    #[test]
    fn bip39_dash_separator() {
        let req = GeneratorRequest::Bip39 {
            word_count: 4,
            separator: "-".into(),
            capitalization: Capitalization::Lower,
        };
        let pw = generate_passphrase(&req).unwrap();
        assert_eq!(pw.split('-').count(), 4);
        assert!(!pw.contains(' '));
    }

    #[test]
    fn bip39_first_of_each_capitalizes() {
        let req = GeneratorRequest::Bip39 {
            word_count: 5,
            separator: " ".into(),
            capitalization: Capitalization::FirstOfEach,
        };
        let pw = generate_passphrase(&req).unwrap();
        for word in pw.split(' ') {
            let first = word.chars().next().unwrap();
            assert!(first.is_ascii_uppercase(), "word {word} should start uppercase");
        }
    }

    #[test]
    fn bip39_rejects_bad_word_count() {
        let req = GeneratorRequest::Bip39 {
            word_count: 2,
            separator: " ".into(),
            capitalization: Capitalization::Lower,
        };
        assert!(generate_passphrase(&req).is_err());
    }

    #[test]
    fn rate_passphrase_strong_one_passes_gate() {
        // 6-word bip39 passphrase
        let req = GeneratorRequest::Bip39 {
            word_count: 6,
            separator: " ".into(),
            capitalization: Capitalization::Lower,
        };
        let pw = generate_passphrase(&req).unwrap();
        assert!(validate_passphrase_strength(&pw).is_ok());
    }

    #[test]
    fn rate_passphrase_weak_fails_gate() {
        assert!(validate_passphrase_strength("password").is_err());
        assert!(validate_passphrase_strength("12345678").is_err());
        assert!(validate_passphrase_strength("hunter2").is_err());
    }
}

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

    #[test]
    fn random_default_password_is_20_chars() {
        let req = GeneratorRequest::default();
        let pw = generate_password(&req).unwrap();
        assert_eq!(pw.len(), 20);
    }

    #[test]
    fn rejects_zero_length() {
        let req = GeneratorRequest::Random {
            length: 0,
            classes: CharClasses { lower: true, upper: false, digits: false, symbols: false },
            symbol_charset: SymbolCharset::SafeOnly,
        };
        assert!(generate_password(&req).is_err());
    }

    #[test]
    fn rejects_no_classes() {
        let req = GeneratorRequest::Random {
            length: 8,
            classes: CharClasses { lower: false, upper: false, digits: false, symbols: false },
            symbol_charset: SymbolCharset::SafeOnly,
        };
        assert!(generate_password(&req).is_err());
    }

    #[test]
    fn lower_only_password_uses_lowercase() {
        let req = GeneratorRequest::Random {
            length: 100,
            classes: CharClasses { lower: true, upper: false, digits: false, symbols: false },
            symbol_charset: SymbolCharset::SafeOnly,
        };
        let pw = generate_password(&req).unwrap();
        assert!(pw.chars().all(|c| c.is_ascii_lowercase()));
    }

    #[test]
    fn safe_symbols_excludes_quotes_and_brackets() {
        let req = GeneratorRequest::Random {
            length: 128,
            classes: CharClasses { lower: false, upper: false, digits: false, symbols: true },
            symbol_charset: SymbolCharset::SafeOnly,
        };
        let pw = generate_password(&req).unwrap();
        for c in pw.chars() {
            assert!(!matches!(c, '\'' | '"' | '`' | ',' | ';' | ':' | '{' | '}' | '[' | ']' | '<' | '>' | '(' | ')' | '|' | '\\' | '/' | '?'),
                    "safe charset must not include {c}");
        }
    }

    #[test]
    fn custom_charset_rejects_non_ascii() {
        let req = GeneratorRequest::Random {
            length: 8,
            classes: CharClasses { lower: false, upper: false, digits: false, symbols: true },
            symbol_charset: SymbolCharset::Custom("ñé".into()),
        };
        let err = generate_password(&req);
        assert!(err.is_err(), "non-ASCII custom charset must be rejected");
    }
}
