//! User-Agent string parser.
//!
//! This module provides high-performance parsing of User-Agent strings
//! to extract browser brand, version, platform, and mobile status.

use crate::{Brand, ClientHints, Platform};
use std::error::Error;
use std::fmt;

/// Error type for User-Agent parsing failures.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ParseError {
    /// The User-Agent string is empty.
    EmptyUserAgent,
    /// No Chromium-based browser was detected.
    NotChromiumBased,
    /// Failed to parse the version number.
    InvalidVersion,
    /// The Chrome/Chromium token was not found.
    ChromeTokenNotFound,
}

impl fmt::Display for ParseError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            ParseError::EmptyUserAgent => write!(f, "empty user agent string"),
            ParseError::NotChromiumBased => {
                write!(f, "not a Chromium-based browser")
            }
            ParseError::InvalidVersion => write!(f, "invalid version number"),
            ParseError::ChromeTokenNotFound => {
                write!(f, "Chrome/Chromium token not found in user agent")
            }
        }
    }
}

impl Error for ParseError {}

/// Parse a User-Agent string into ClientHints.
///
/// This function extracts the browser brand, version, platform, and mobile
/// status from a User-Agent string.
pub fn parse(user_agent: &str) -> Result<ClientHints, ParseError> {
    if user_agent.is_empty() {
        return Err(ParseError::EmptyUserAgent);
    }

    // Detect platform first
    let platform = detect_platform(user_agent);

    // Detect if mobile
    let is_mobile = detect_mobile(user_agent);

    // Extract Chrome/Chromium version (required for all Chromium-based browsers)
    let (chromium_version, major_version) = extract_chromium_version(user_agent)?;

    // Detect specific browser brand
    let brand = detect_brand(user_agent);

    Ok(ClientHints::new(
        brand,
        major_version,
        chromium_version,
        platform,
        is_mobile,
    ))
}

/// Detect the platform from the User-Agent string.
fn detect_platform(ua: &str) -> Platform {
    // Check in order of specificity
    if ua.contains("Android") {
        Platform::Android
    } else if ua.contains("CrOS") {
        Platform::ChromeOS
    } else if ua.contains("Windows") {
        Platform::Windows
    } else if ua.contains("Macintosh") || ua.contains("Mac OS") {
        Platform::MacOS
    } else if ua.contains("Linux") || ua.contains("X11") {
        Platform::Linux
    } else {
        Platform::Unknown
    }
}

/// Detect if the browser is mobile.
fn detect_mobile(ua: &str) -> bool {
    // Common mobile indicators in User-Agent strings
    ua.contains("Mobile") || ua.contains("Android") && !ua.contains("Tablet")
}

/// Extract the Chromium version from the User-Agent string.
///
/// Returns (full_version, major_version).
fn extract_chromium_version(ua: &str) -> Result<(String, u32), ParseError> {
    // Look for Chrome/ or Chromium/ token
    let chrome_pos = ua
        .find("Chrome/")
        .or_else(|| ua.find("Chromium/"))
        .ok_or(ParseError::ChromeTokenNotFound)?;

    // Extract the version string
    let version_start = chrome_pos + if ua[chrome_pos..].starts_with("Chromium/") { 9 } else { 7 };
    let version_end = ua[version_start..]
        .find(|c: char| !c.is_ascii_digit() && c != '.')
        .map(|i| version_start + i)
        .unwrap_or(ua.len());

    let version_str = &ua[version_start..version_end];

    if version_str.is_empty() {
        return Err(ParseError::InvalidVersion);
    }

    // Extract major version
    let major_str = version_str
        .split('.')
        .next()
        .ok_or(ParseError::InvalidVersion)?;

    let major_version = major_str
        .parse::<u32>()
        .map_err(|_| ParseError::InvalidVersion)?;

    Ok((version_str.to_string(), major_version))
}

/// Detect the browser brand from the User-Agent string.
///
/// Checks for specific browser identifiers in order of priority.
fn detect_brand(ua: &str) -> Brand {
    // Check for specific browsers in order of priority
    // Some browsers include both their identifier and Chrome, so check specific ones first

    // Edge: contains "Edg/" (not "Edge/" which is legacy EdgeHTML)
    if ua.contains("Edg/") || ua.contains("EdgA/") || ua.contains("EdgiOS/") {
        return Brand::Edge;
    }

    // Brave: may contain "Brave" in navigator or may not (for fingerprint protection)
    // Often detected via Brave-specific patterns
    if ua.contains("Brave") {
        return Brand::Brave;
    }

    // Opera: contains "OPR/" or "Opera/"
    if ua.contains("OPR/") || ua.contains("Opera/") {
        return Brand::Opera;
    }

    // Vivaldi: contains "Vivaldi/"
    if ua.contains("Vivaldi/") {
        return Brand::Vivaldi;
    }

    // Samsung Internet: contains "SamsungBrowser/"
    if ua.contains("SamsungBrowser/") {
        return Brand::Samsung;
    }

    // Yandex: contains "YaBrowser/" or "Yandex/"
    if ua.contains("YaBrowser/") || ua.contains("Yandex/") {
        return Brand::Yandex;
    }

    // Generic Chromium: contains "Chromium/"
    if ua.contains("Chromium/") {
        return Brand::Chromium;
    }

    // Default to Chrome if Chrome/ is present
    if ua.contains("Chrome/") {
        return Brand::Chrome;
    }

    // Fallback to Chromium
    Brand::Chromium
}

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

    #[test]
    fn test_empty_ua() {
        assert_eq!(parse(""), Err(ParseError::EmptyUserAgent));
    }

    #[test]
    fn test_firefox() {
        let ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/119.0";
        assert_eq!(parse(ua), Err(ParseError::ChromeTokenNotFound));
    }

    #[test]
    fn test_safari() {
        let ua = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Safari/605.1.15";
        assert_eq!(parse(ua), Err(ParseError::ChromeTokenNotFound));
    }

    #[test]
    fn test_chrome_windows() {
        let ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.6099.129 Safari/537.36";
        let hints = parse(ua).unwrap();

        assert_eq!(hints.brand(), Brand::Chrome);
        assert_eq!(hints.major_version(), 120);
        assert_eq!(hints.full_version(), "120.0.6099.129");
        assert_eq!(hints.platform(), Platform::Windows);
        assert!(!hints.is_mobile());
    }

    #[test]
    fn test_chrome_macos() {
        let ua = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
        let hints = parse(ua).unwrap();
        assert_eq!(hints.brand(), Brand::Chrome);
        assert_eq!(hints.platform(), Platform::MacOS);
        assert!(!hints.is_mobile());
    }

    #[test]
    fn test_chrome_linux() {
        let ua = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
        let hints = parse(ua).unwrap();

        assert_eq!(hints.brand(), Brand::Chrome);
        assert_eq!(hints.platform(), Platform::Linux);
    }

    #[test]
    fn test_chrome_android() {
        let ua = "Mozilla/5.0 (Linux; Android 13; SM-G998B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.6099.129 Mobile Safari/537.36";
        let hints = parse(ua).unwrap();

        assert_eq!(hints.brand(), Brand::Chrome);
        assert_eq!(hints.platform(), Platform::Android);
        assert!(hints.is_mobile());
    }

    #[test]
    fn test_edge_windows() {
        let ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.2210.91";
        let hints = parse(ua).unwrap();

        assert_eq!(hints.brand(), Brand::Edge);
        assert_eq!(hints.major_version(), 120);
        assert_eq!(hints.platform(), Platform::Windows);
    }

    #[test]
    fn test_edge_android() {
        let ua = "Mozilla/5.0 (Linux; Android 10; SM-G975F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36 EdgA/120.0.0.0";
        let hints = parse(ua).unwrap();

        assert_eq!(hints.brand(), Brand::Edge);
        assert_eq!(hints.platform(), Platform::Android);
        assert!(hints.is_mobile());
    }

    #[test]
    fn test_brave() {
        let ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Brave/120";
        let hints = parse(ua).unwrap();

        assert_eq!(hints.brand(), Brand::Brave);
    }

    #[test]
    fn test_opera() {
        let ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 OPR/106.0.0.0";
        let hints = parse(ua).unwrap();

        assert_eq!(hints.brand(), Brand::Opera);
    }

    #[test]
    fn test_vivaldi() {
        let ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Vivaldi/6.4.3160.47";
        let hints = parse(ua).unwrap();

        assert_eq!(hints.brand(), Brand::Vivaldi);
    }

    #[test]
    fn test_samsung() {
        let ua = "Mozilla/5.0 (Linux; Android 13; SM-G998B) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/23.0 Chrome/115.0.0.0 Mobile Safari/537.36";
        let hints = parse(ua).unwrap();

        assert_eq!(hints.brand(), Brand::Samsung);
        assert!(hints.is_mobile());
    }

    #[test]
    fn test_yandex() {
        let ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 YaBrowser/24.1.0.0 Safari/537.36";
        let hints = parse(ua).unwrap();

        assert_eq!(hints.brand(), Brand::Yandex);
    }

    #[test]
    fn test_chromium() {
        let ua = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chromium/120.0.0.0 Safari/537.36";
        let hints = parse(ua).unwrap();

        assert_eq!(hints.brand(), Brand::Chromium);
    }

    #[test]
    fn test_chrome_os() {
        let ua = "Mozilla/5.0 (X11; CrOS x86_64 14541.0.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
        let hints = parse(ua).unwrap();

        assert_eq!(hints.platform(), Platform::ChromeOS);
    }

    #[test]
    fn test_version_extraction() {
        let ua = "Mozilla/5.0 Chrome/119.0.5678.90";
        let (full, major) = extract_chromium_version(ua).unwrap();

        assert_eq!(full, "119.0.5678.90");
        assert_eq!(major, 119);
    }

    #[test]
    fn test_platform_detection() {
        assert_eq!(detect_platform("Windows NT 10.0"), Platform::Windows);
        assert_eq!(detect_platform("Macintosh; Intel Mac OS X"), Platform::MacOS);
        assert_eq!(detect_platform("X11; Linux x86_64"), Platform::Linux);
        assert_eq!(detect_platform("Linux; Android 13"), Platform::Android);
        assert_eq!(detect_platform("CrOS x86_64"), Platform::ChromeOS);
        assert_eq!(detect_platform("Unknown OS"), Platform::Unknown);
    }

    #[test]
    fn test_mobile_detection() {
        assert!(detect_mobile("Mobile Safari/537.36"));
        assert!(detect_mobile("Linux; Android 13"));
        assert!(!detect_mobile("Windows NT 10.0"));
        assert!(!detect_mobile("Macintosh"));
    }

    #[test]
    fn test_parse_error_display() {
        assert_eq!(
            format!("{}", ParseError::EmptyUserAgent),
            "empty user agent string"
        );
        assert_eq!(
            format!("{}", ParseError::NotChromiumBased),
            "not a Chromium-based browser"
        );
        assert_eq!(
            format!("{}", ParseError::InvalidVersion),
            "invalid version number"
        );
        assert_eq!(
            format!("{}", ParseError::ChromeTokenNotFound),
            "Chrome/Chromium token not found in user agent"
        );
    }
}
