//! Input validation utilities

use crate::errors::AppError;
use serde_json::Value;
use std::collections::HashSet;
use std::net::Ipv4Addr;
use std::sync::OnceLock;

/// Built-in disposable email domain blocklist, loaded once from the vendored
/// data file (~5,000 domains, MIT-licensed from disposable-email-domains).
static DISPOSABLE_DOMAINS: OnceLock<HashSet<&str>> = OnceLock::new();

fn disposable_domains() -> &'static HashSet<&'static str> {
    DISPOSABLE_DOMAINS.get_or_init(|| {
        include_str!("../data/disposable_domains.txt")
            .lines()
            .filter(|l| !l.is_empty() && !l.starts_with('#'))
            .collect()
    })
}

/// Number of built-in disposable domains (for admin info endpoint).
pub fn disposable_domain_count() -> usize {
    disposable_domains().len()
}

/// Check if an email domain is a known disposable email provider.
///
/// Checks the built-in blocklist first, then the optional custom domain set.
pub fn is_disposable_email(email: &str, custom_domains: Option<&HashSet<String>>) -> bool {
    let domain = match email.rsplit_once('@') {
        Some((_, domain)) => domain.to_lowercase(),
        None => return false,
    };

    if disposable_domains().contains(domain.as_str()) {
        return true;
    }

    if let Some(custom) = custom_domains {
        return custom.contains(&domain);
    }

    false
}

/// S-11: Common typo TLDs that are likely user errors, not intentional.
/// These are rejected to prevent delivery failures and user frustration.
const TYPO_TLDS: &[&str] = &[
    // .com typos
    "con", // common keyboard typo
    "cmo", // transposition
    "ocm", // transposition
    "cm",  // Cameroon - often accidental .com typo
    "vom", // v/c swap on QWERTY
    "xom", // x/c swap on QWERTY
    "cpm", // p/o swap
    "clm", // l/o swap
    // .net typos
    "ney", // t/y swap
    "met", // n/m swap
    "bet", // n/b swap
    "nrt", // e/r swap
    // .org typos
    "ogr", // transposition
    "rog", // transposition
    "prg", // o/p swap
    "irg", // o/i swap
    // .edu typos
    "edi", // u/i swap
    "rdu", // e/r swap
];

/// Check if a TLD is a known typo
fn is_typo_tld(tld: &str) -> bool {
    TYPO_TLDS.contains(&tld)
}

/// Validate email address format
///
/// Checks for:
/// - Presence of exactly one @ symbol
/// - Non-empty local part (before @)
/// - Non-empty domain part (after @)
/// - Domain contains at least one dot
/// - No spaces in the email
/// - Reasonable length constraints
pub fn is_valid_email(email: &str) -> bool {
    // Check basic length constraints
    if email.is_empty() || email.len() > 254 {
        return false;
    }

    // No spaces allowed
    if email.contains(' ') {
        return false;
    }

    // Split by @ - must have exactly one
    let parts: Vec<&str> = email.split('@').collect();
    if parts.len() != 2 {
        return false;
    }

    let local = parts[0];
    let domain = parts[1];

    // Local part validation
    if local.is_empty() || local.len() > 64 {
        return false;
    }

    // Local part can't start or end with a dot
    if local.starts_with('.') || local.ends_with('.') {
        return false;
    }

    // B-14: RFC 5321 disallows consecutive dots in local part
    if local.contains("..") {
        return false;
    }

    // Domain validation
    if domain.is_empty() || domain.len() > 253 {
        return false;
    }

    // Domain must contain at least one dot
    if !domain.contains('.') {
        return false;
    }

    // Domain can't start or end with a dot or hyphen
    if domain.starts_with('.')
        || domain.ends_with('.')
        || domain.starts_with('-')
        || domain.ends_with('-')
    {
        return false;
    }

    // Check each domain label doesn't start or end with hyphen
    for label in domain.split('.') {
        if label.starts_with('-') || label.ends_with('-') {
            return false;
        }
    }

    // Check TLD (part after last dot) is at least 2 chars
    if let Some(tld) = domain.rsplit('.').next() {
        if tld.len() < 2 {
            return false;
        }
        // TLD must be alphabetic (no numbers)
        if !tld.chars().all(|c| c.is_ascii_alphabetic()) {
            return false;
        }
        // S-11: Deny common typo TLDs to prevent user errors
        let tld_lower = tld.to_ascii_lowercase();
        if is_typo_tld(&tld_lower) {
            return false;
        }
    }

    // Domain parts can only contain alphanumeric, dots, and hyphens
    if !domain
        .chars()
        .all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-')
    {
        return false;
    }

    // Local part can contain alphanumeric, dots, and certain special chars
    // Simplified check: allow common characters
    if !local.chars().all(|c| {
        c.is_ascii_alphanumeric() || c == '.' || c == '_' || c == '-' || c == '+' || c == '!'
    }) {
        return false;
    }

    true
}

/// Validate that a string looks like an IPv4 address
pub fn is_valid_ipv4(ip: &str) -> bool {
    ip.parse::<Ipv4Addr>().is_ok()
}

// =============================================================================
// Metadata Validation
// =============================================================================

/// Common secret key name patterns (case-insensitive)
const SECRET_KEY_PATTERNS: &[&str] = &[
    "password",
    "passwd",
    "secret",
    "api_key",
    "apikey",
    "api-key",
    "token",
    "auth",
    "credential",
    "private_key",
    "privatekey",
    "private-key",
    "access_key",
    "accesskey",
    "access-key",
    "secret_key",
    "secretkey",
    "secret-key",
    "bearer",
    "jwt",
    "session",
    "cookie",
    "authorization",
];

/// Patterns that indicate a value might be a secret
const SECRET_VALUE_PATTERNS: &[&str] = &[
    "sk_live_",   // Stripe live key
    "sk_test_",   // Stripe test key
    "pk_live_",   // Stripe publishable
    "pk_test_",   // Stripe publishable
    "ghp_",       // GitHub personal access token
    "gho_",       // GitHub OAuth token
    "ghu_",       // GitHub user token
    "ghs_",       // GitHub server token
    "AKIA",       // AWS access key
    "eyJ",        // JWT header (base64 encoded)
    "bearer ",    // Bearer token
    "basic ",     // Basic auth
    "AIza",       // Google API key
    "xox",        // Slack token
    "ssh-rsa",    // SSH key
    "-----BEGIN", // PEM encoded key
];

/// SRV-13: Known reference types for credit operations
const ALLOWED_REFERENCE_TYPES: &[&str] = &[
    "order",
    "subscription",
    "refund",
    "bonus",
    "promo",
    "correction",
    "deposit",
    "withdrawal",
];

/// SRV-13: Validate reference_type against known whitelist
pub fn validate_reference_type(reference_type: &str) -> Result<(), AppError> {
    if !ALLOWED_REFERENCE_TYPES.contains(&reference_type) {
        return Err(AppError::Validation(format!(
            "Unknown reference_type '{}'. Allowed: {}",
            reference_type,
            ALLOWED_REFERENCE_TYPES.join(", ")
        )));
    }
    Ok(())
}

/// SRV-14: Allowed credit currencies
const ALLOWED_CURRENCIES: &[&str] = &["SOL", "USD"];

/// SRV-14: Validate currency against known whitelist.
/// The credit system uses "SOL" and "USD" internally (USDC/USDT map to "USD" at the deposit layer).
pub fn validate_currency(currency: &str) -> Result<(), AppError> {
    if !ALLOWED_CURRENCIES.contains(&currency) {
        return Err(AppError::Validation(format!(
            "Unknown currency '{}'. Allowed: {}",
            currency,
            ALLOWED_CURRENCIES.join(", ")
        )));
    }
    Ok(())
}

/// Validate metadata does not contain obvious secrets
///
/// Returns Ok(()) if metadata is safe, or an error describing the issue.
/// Checks both key names and string values for secret patterns.
pub fn validate_metadata_no_secrets(metadata: Option<&Value>) -> Result<(), AppError> {
    let Some(value) = metadata else {
        return Ok(());
    };

    validate_value_recursive(value, 0)
}

fn validate_value_recursive(value: &Value, depth: usize) -> Result<(), AppError> {
    // Prevent deep nesting attacks
    if depth > 10 {
        return Err(AppError::Validation(
            "Metadata nesting too deep (max 10 levels)".into(),
        ));
    }

    match value {
        Value::Object(map) => {
            for (key, val) in map.iter() {
                // Check key name for secret patterns
                let key_lower = key.to_lowercase();
                for pattern in SECRET_KEY_PATTERNS {
                    if key_lower.contains(pattern) {
                        return Err(AppError::Validation(format!(
                            "Metadata key '{}' appears to contain sensitive data. \
                             Do not store secrets in metadata.",
                            key
                        )));
                    }
                }

                // Recursively check value
                validate_value_recursive(val, depth + 1)?;
            }
        }
        Value::Array(arr) => {
            // Limit array size
            if arr.len() > 100 {
                return Err(AppError::Validation(
                    "Metadata array too large (max 100 elements)".into(),
                ));
            }
            for item in arr {
                validate_value_recursive(item, depth + 1)?;
            }
        }
        Value::String(s) => {
            // Check string length
            if s.len() > 10_000 {
                return Err(AppError::Validation(
                    "Metadata string value too long (max 10000 chars)".into(),
                ));
            }

            // Check for secret value patterns
            let s_lower = s.to_lowercase();
            for pattern in SECRET_VALUE_PATTERNS {
                if s_lower.starts_with(&pattern.to_lowercase()) {
                    return Err(AppError::Validation(
                        "Metadata value appears to contain a secret or API key. \
                         Do not store secrets in metadata."
                            .into(),
                    ));
                }
            }
        }
        _ => {}
    }

    Ok(())
}

/// Validate a Solana wallet address format.
///
/// Solana addresses are base58-encoded 32-byte public keys, typically 32-44 characters.
/// This performs basic format validation but does not verify the key is on the curve.
pub fn is_valid_wallet_address(address: &str) -> bool {
    // Solana addresses are typically 32-44 characters (base58 encoding of 32 bytes)
    if address.len() < 32 || address.len() > 44 {
        return false;
    }

    // Must be valid base58 characters (1-9, A-H, J-N, P-Z, a-k, m-z)
    // Excludes: 0, O, I, l (to avoid ambiguity)
    const BASE58_CHARS: &str = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
    address.chars().all(|c| BASE58_CHARS.contains(c))
}

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

    #[test]
    fn test_valid_emails() {
        assert!(is_valid_email("test@example.com"));
        assert!(is_valid_email("user.name@example.com"));
        assert!(is_valid_email("user+tag@example.com"));
        assert!(is_valid_email("user_name@example.co.uk"));
        assert!(is_valid_email("a@b.co"));
        assert!(is_valid_email("test123@test123.com"));
        assert!(is_valid_email("user-name@example.org"));
        assert!(is_valid_email("user!@example.com"));
    }

    #[test]
    fn test_invalid_emails_no_at() {
        assert!(!is_valid_email("testexample.com"));
        assert!(!is_valid_email("test"));
    }

    #[test]
    fn test_invalid_emails_multiple_at() {
        assert!(!is_valid_email("test@@example.com"));
        assert!(!is_valid_email("test@test@example.com"));
    }

    #[test]
    fn test_invalid_emails_empty_parts() {
        assert!(!is_valid_email("@example.com"));
        assert!(!is_valid_email("test@"));
        assert!(!is_valid_email("@"));
        assert!(!is_valid_email(""));
    }

    #[test]
    fn test_invalid_emails_no_tld() {
        assert!(!is_valid_email("test@example"));
        assert!(!is_valid_email("test@localhost"));
    }

    #[test]
    fn test_invalid_emails_short_tld() {
        assert!(!is_valid_email("test@example.c"));
    }

    #[test]
    fn test_invalid_emails_numeric_tld() {
        assert!(!is_valid_email("test@example.123"));
    }

    #[test]
    fn test_invalid_emails_spaces() {
        assert!(!is_valid_email("test @example.com"));
        assert!(!is_valid_email("test@ example.com"));
        assert!(!is_valid_email(" test@example.com"));
        assert!(!is_valid_email("test@example.com "));
    }

    #[test]
    fn test_invalid_emails_invalid_chars() {
        assert!(!is_valid_email("test<>@example.com"));
        assert!(!is_valid_email("test@exam ple.com"));
    }

    #[test]
    fn test_invalid_emails_dots() {
        assert!(!is_valid_email(".test@example.com"));
        assert!(!is_valid_email("test.@example.com"));
        assert!(!is_valid_email("test@.example.com"));
        assert!(!is_valid_email("test@example.com."));
        // B-14: Consecutive dots are invalid per RFC 5321
        assert!(!is_valid_email("user..name@example.com"));
        assert!(!is_valid_email("user...name@example.com"));
    }

    #[test]
    fn test_invalid_emails_domain_hyphens() {
        assert!(!is_valid_email("test@-example.com"));
        assert!(!is_valid_email("test@example-.com"));
    }

    #[test]
    fn test_invalid_emails_too_long() {
        let long_local = "a".repeat(65);
        let long_email = format!("{}@example.com", long_local);
        assert!(!is_valid_email(&long_email));

        let very_long_email = format!("test@{}.com", "a".repeat(250));
        assert!(!is_valid_email(&very_long_email));
    }

    #[test]
    fn test_valid_ipv4() {
        assert!(is_valid_ipv4("127.0.0.1"));
        assert!(is_valid_ipv4("192.168.1.1"));
        assert!(is_valid_ipv4("0.0.0.0"));
        assert!(is_valid_ipv4("255.255.255.255"));
    }

    #[test]
    fn test_invalid_ipv4() {
        assert!(!is_valid_ipv4("256.0.0.1"));
        assert!(!is_valid_ipv4("192.168.1"));
        assert!(!is_valid_ipv4("192.168.1.1.1"));
        assert!(!is_valid_ipv4("not.an.ip.address"));
        assert!(!is_valid_ipv4(""));
    }

    #[test]
    fn test_typo_tlds_rejected() {
        // .com typos
        assert!(!is_valid_email("test@example.con"));
        assert!(!is_valid_email("test@example.cmo"));
        assert!(!is_valid_email("test@example.cm"));
        assert!(!is_valid_email("test@example.vom"));
        // .net typos
        assert!(!is_valid_email("test@example.ney"));
        assert!(!is_valid_email("test@example.met"));
        // .org typos
        assert!(!is_valid_email("test@example.ogr"));
        assert!(!is_valid_email("test@example.prg"));
        // Case insensitive
        assert!(!is_valid_email("test@example.CON"));
        assert!(!is_valid_email("test@example.Con"));
    }

    #[test]
    fn test_valid_tlds_allowed() {
        // Real TLDs should still work
        assert!(is_valid_email("test@example.com"));
        assert!(is_valid_email("test@example.net"));
        assert!(is_valid_email("test@example.org"));
        assert!(is_valid_email("test@example.edu"));
        assert!(is_valid_email("test@example.io"));
        assert!(is_valid_email("test@example.co"));
        assert!(is_valid_email("test@example.co.uk"));
    }

    // =========================================================================
    // Metadata Validation Tests
    // =========================================================================

    #[test]
    fn test_metadata_none_is_valid() {
        assert!(validate_metadata_no_secrets(None).is_ok());
    }

    #[test]
    fn test_metadata_safe_values_allowed() {
        use serde_json::json;

        let metadata = json!({
            "order_id": "12345",
            "items": ["widget", "gadget"],
            "quantity": 5,
            "customer_note": "Please ship quickly"
        });

        assert!(validate_metadata_no_secrets(Some(&metadata)).is_ok());
    }

    #[test]
    fn test_metadata_rejects_password_key() {
        use serde_json::json;

        let metadata = json!({
            "password": "my-secret"
        });

        let result = validate_metadata_no_secrets(Some(&metadata));
        assert!(result.is_err());
        assert!(result.unwrap_err().to_string().contains("password"));
    }

    #[test]
    fn test_metadata_rejects_api_key() {
        use serde_json::json;

        let metadata = json!({
            "api_key": "sk_live_xxx"
        });

        assert!(validate_metadata_no_secrets(Some(&metadata)).is_err());
    }

    #[test]
    fn test_metadata_rejects_secret_key_pattern() {
        use serde_json::json;

        let metadata = json!({
            "user_secret": "abc123"
        });

        assert!(validate_metadata_no_secrets(Some(&metadata)).is_err());
    }

    #[test]
    fn test_metadata_rejects_stripe_key_value() {
        use serde_json::json;

        let metadata = json!({
            "notes": "sk_live_abcdefg12345"
        });

        assert!(validate_metadata_no_secrets(Some(&metadata)).is_err());
    }

    #[test]
    fn test_metadata_rejects_jwt_value() {
        use serde_json::json;

        let metadata = json!({
            "data": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.xxx"
        });

        assert!(validate_metadata_no_secrets(Some(&metadata)).is_err());
    }

    #[test]
    fn test_metadata_rejects_aws_key_value() {
        use serde_json::json;

        let metadata = json!({
            "info": "AKIAIOSFODNN7EXAMPLE"
        });

        assert!(validate_metadata_no_secrets(Some(&metadata)).is_err());
    }

    #[test]
    fn test_metadata_rejects_nested_secret() {
        use serde_json::json;

        let metadata = json!({
            "user": {
                "profile": {
                    "api_token": "secret123"
                }
            }
        });

        assert!(validate_metadata_no_secrets(Some(&metadata)).is_err());
    }

    #[test]
    fn test_metadata_rejects_too_deep_nesting() {
        use serde_json::json;

        // Create deeply nested structure
        let mut value = json!("leaf");
        for _ in 0..15 {
            value = json!({ "nested": value });
        }

        let result = validate_metadata_no_secrets(Some(&value));
        assert!(result.is_err());
        assert!(result.unwrap_err().to_string().contains("nesting"));
    }

    #[test]
    fn test_metadata_rejects_large_array() {
        use serde_json::json;

        let large_array: Vec<i32> = (0..150).collect();
        let metadata = json!({ "items": large_array });

        let result = validate_metadata_no_secrets(Some(&metadata));
        assert!(result.is_err());
        assert!(result.unwrap_err().to_string().contains("array"));
    }

    #[test]
    fn test_metadata_rejects_long_string() {
        use serde_json::json;

        let long_string = "x".repeat(15_000);
        let metadata = json!({ "data": long_string });

        let result = validate_metadata_no_secrets(Some(&metadata));
        assert!(result.is_err());
        assert!(result.unwrap_err().to_string().contains("string"));
    }

    // =========================================================================
    // Disposable Email Tests
    // =========================================================================

    #[test]
    fn test_disposable_email_detected() {
        assert!(is_disposable_email("test@mailinator.com", None));
        assert!(is_disposable_email("user@guerrillamail.com", None));
        assert!(is_disposable_email("temp@10minutemail.com", None));
        assert!(is_disposable_email("spam@yopmail.com", None));
        assert!(is_disposable_email("throwaway@tempmail.it", None));
    }

    #[test]
    fn test_disposable_email_case_insensitive() {
        assert!(is_disposable_email("test@MAILINATOR.COM", None));
        assert!(is_disposable_email("test@Mailinator.Com", None));
        assert!(is_disposable_email("test@MailInator.COM", None));
    }

    #[test]
    fn test_legitimate_email_allowed() {
        assert!(!is_disposable_email("user@gmail.com", None));
        assert!(!is_disposable_email("user@outlook.com", None));
        assert!(!is_disposable_email("user@yahoo.com", None));
        assert!(!is_disposable_email("user@company.com", None));
        assert!(!is_disposable_email("user@university.edu", None));
    }

    #[test]
    fn test_disposable_email_invalid_input() {
        // Invalid emails should return false (not disposable)
        assert!(!is_disposable_email("notanemail", None));
        assert!(!is_disposable_email("", None));
        assert!(!is_disposable_email("@", None));
    }

    #[test]
    fn test_disposable_email_custom_domains() {
        let custom = HashSet::from(["blocked.example.com".to_string()]);
        assert!(is_disposable_email("user@blocked.example.com", Some(&custom)));
        assert!(!is_disposable_email("user@allowed.example.com", Some(&custom)));
    }

    #[test]
    fn test_disposable_domain_count() {
        assert!(disposable_domain_count() > 3000);
    }

    // =========================================================================
    // Wallet Address Validation Tests
    // =========================================================================

    #[test]
    fn test_valid_wallet_addresses() {
        // Valid Solana addresses (base58, 32-44 chars)
        assert!(is_valid_wallet_address(
            "6o6HrBfnmzpQsMJHJZuQTFhBnXPKadjFnPkKB7p2AFSL"
        ));
        assert!(is_valid_wallet_address("11111111111111111111111111111111"));
        assert!(is_valid_wallet_address(
            "So11111111111111111111111111111111111111112"
        ));
    }

    #[test]
    fn test_invalid_wallet_too_short() {
        assert!(!is_valid_wallet_address("1234567890123456789012345678901")); // 31 chars
        assert!(!is_valid_wallet_address("abc"));
        assert!(!is_valid_wallet_address(""));
    }

    #[test]
    fn test_invalid_wallet_too_long() {
        assert!(!is_valid_wallet_address(
            "123456789012345678901234567890123456789012345"
        )); // 45 chars
    }

    #[test]
    fn test_invalid_wallet_invalid_chars() {
        // Contains 0 (zero), O, I, l - not valid base58
        assert!(!is_valid_wallet_address(
            "0o6HrBfnmzpQsMJHJZuQTFhBnXPKadjFnPkKB7p2AFSL"
        )); // 0
        assert!(!is_valid_wallet_address(
            "Oo6HrBfnmzpQsMJHJZuQTFhBnXPKadjFnPkKB7p2AFSL"
        )); // O
        assert!(!is_valid_wallet_address(
            "Io6HrBfnmzpQsMJHJZuQTFhBnXPKadjFnPkKB7p2AFSL"
        )); // I
        assert!(!is_valid_wallet_address(
            "lo6HrBfnmzpQsMJHJZuQTFhBnXPKadjFnPkKB7p2AFSL"
        )); // l
    }
}
