//! HTML parsing and manipulation utilities.
//!
//! This module provides convenience functions for working with HTML content.
//! It uses `html5ever` for robust HTML parsing.

use crate::error::{WebError, WebResult};
use regex::Regex;
use std::sync::OnceLock;

/// Struct for building and validating HTML content.
#[derive(Debug, Clone, Default)]
pub struct HtmlBuilder {
    content: String,
}

impl HtmlBuilder {
    /// Create a new HTML builder.
    pub fn new() -> Self {
        Self::default()
    }

    /// Add a tag to the HTML content.
    ///
    /// # Arguments
    ///
    /// * `tag` - The HTML tag name
    /// * `content` - The content inside the tag
    /// * `attributes` - Optional attributes as key-value pairs
    ///
    /// # Example
    ///
    /// ```ignore
    /// let html = HtmlBuilder::new()
    ///     .add_tag("p", "Hello World", &[])
    ///     .build();
    /// ```
    pub fn add_tag(mut self, tag: &str, content: &str, attributes: &[(&str, &str)]) -> Self {
        let attrs = if attributes.is_empty() {
            String::new()
        } else {
            let attr_str = attributes
                .iter()
                .map(|(k, v)| format!(r#"{}="{}""#, k, v))
                .collect::<Vec<_>>()
                .join(" ");
            format!(" {}", attr_str)
        };
        self.content.push_str(&format!("<{tag}{attrs}>{}</{tag}>", content));
        self
    }

    /// Add raw HTML content.
    pub fn add_raw(mut self, content: &str) -> Self {
        self.content.push_str(content);
        self
    }

    /// Build and return the HTML content.
    pub fn build(self) -> String {
        self.content
    }

    /// Build and validate the HTML content.
    pub fn build_validated(self) -> WebResult<String> {
        validate_html(&self.content)?;
        Ok(self.content)
    }
}

/// Validate HTML structure.
///
/// Performs basic validation on HTML tags.
pub fn validate_html(html: &str) -> WebResult<()> {
    let open_tag_regex = regex_open_tag();
    let close_tag_regex = regex_close_tag();

    let open_tags: Vec<&str> = open_tag_regex
        .captures_iter(html)
        .filter_map(|c| c.get(1).map(|m| m.as_str()))
        .collect();

    let close_tags: Vec<&str> = close_tag_regex
        .captures_iter(html)
        .filter_map(|c| c.get(1).map(|m| m.as_str()))
        .collect();

    // Basic self-closing tags that don't need closing tags
    let self_closing = ["br", "hr", "img", "input", "meta", "link"];

    let mut open_stack: Vec<&str> = Vec::new();

    for tag in &open_tags {
        if !self_closing.contains(tag) {
            open_stack.push(tag);
        }
    }

    for tag in &close_tags {
        if let Some(last) = open_stack.pop()
            && last != *tag
        {
            return Err(WebError::invalid_html(format!(
                "Mismatched tags: expected '{}' but found '{}'",
                last, tag
            )));
        }
    }

    if !open_stack.is_empty() {
        let unclosed = open_stack.join(", ");
        return Err(WebError::invalid_html(format!(
            "Unclosed tags: {}",
            unclosed
        )));
    }

    Ok(())
}

/// Minify HTML by removing unnecessary whitespace.
///
/// # Arguments
///
/// * `html` - The HTML content to minify
///
/// # Returns
///
/// Minified HTML string
pub fn minify_html(html: &str) -> String {
    // Remove newlines and extra spaces
    let whitespace_regex = regex_whitespace();
    whitespace_regex.replace_all(html, " ").to_string().trim().to_string()
}

/// Escape HTML special characters.
///
/// Converts HTML special characters to their entity representations.
pub fn escape_html(text: &str) -> String {
    text.chars()
        .map(|c| match c {
            '&' => "&amp;".to_string(),
            '<' => "&lt;".to_string(),
            '>' => "&gt;".to_string(),
            '"' => "&quot;".to_string(),
            '\'' => "&#39;".to_string(),
            c => c.to_string(),
        })
        .collect()
}

/// Unescape HTML entities.
///
/// Converts HTML entity representations back to their character forms.
pub fn unescape_html(text: &str) -> String {
    text.replace("&amp;", "&")
        .replace("&lt;", "<")
        .replace("&gt;", ">")
        .replace("&quot;", "\"")
        .replace("&#39;", "'")
}

/// Extract all text content from HTML, removing tags.
///
/// # Arguments
///
/// * `html` - The HTML content
///
/// # Returns
///
/// Text content without HTML tags
pub fn extract_text(html: &str) -> String {
    let tag_regex = regex_tags();
    tag_regex.replace_all(html, "").to_string()
}

/// Bootstrap 5.3.8 CSS CDN link
const BOOTSTRAP_CSS: &str = r#"<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">"#;

/// Bootstrap 5.3.8 JavaScript CDN link
const BOOTSTRAP_JS: &str = r#"<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js" integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI" crossorigin="anonymous"></script>"#;

/// Font Awesome 7.0.1 CSS CDN link
const FONTAWESOME_CSS: &str = r#"<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.0.1/css/all.min.css">"#;

/// Font Awesome 7.0.1 JavaScript CDN link
const FONTAWESOME_JS: &str = r#"<script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.0.1/js/all.min.js"></script>"#;

/// Add Bootstrap 5.3.8 to an HTML document.
///
/// This function will:
/// - Remove any existing Bootstrap CSS/JS references (from any version)
/// - Add the CSS link in the `<head>` section
/// - Add the JavaScript before the closing `</body>` tag
///
/// If the HTML doesn't have proper head/body structure, the Bootstrap links
/// will be injected at appropriate positions.
///
/// # Arguments
///
/// * `html` - The HTML content
///
/// # Returns
///
/// Modified HTML with Bootstrap 5.3.8 properly injected
pub fn add_bootstrap(html: &str) -> String {
    // Remove existing bootstrap and font-awesome references (any version)
    let mut result = remove_bootstrap(html);

    // Add Bootstrap CSS and Font Awesome CSS to head
    if let Some(head_pos) = result.find("</head>") {
        let css_with_newline = format!("    {}\n    {}\n", BOOTSTRAP_CSS, FONTAWESOME_CSS);
        result.insert_str(head_pos, &css_with_newline);
    } else if let Some(html_pos) = result.find("<html>") {
        // If no head tag, insert after html tag
        let insert_pos = html_pos + 6;
        let head_section = format!("<head>\n    {}\n    {}\n</head>\n", BOOTSTRAP_CSS, FONTAWESOME_CSS);
        result.insert_str(insert_pos, &head_section);
    } else {
        // Last resort: insert at the beginning
        result.insert_str(0, &format!("<head>\n    {}\n    {}\n</head>\n", BOOTSTRAP_CSS, FONTAWESOME_CSS));
    }

    // Add Bootstrap JS and Font Awesome JS before closing body tag
    if let Some(body_pos) = result.rfind("</body>") {
        let js_with_newline = format!("    {}\n    {}\n", BOOTSTRAP_JS, FONTAWESOME_JS);
        result.insert_str(body_pos, &js_with_newline);
    } else if let Some(html_pos) = result.rfind("</html>") {
        // If no body tag, insert before html closing tag
        result.insert_str(html_pos, &format!("<body>\n    {}\n    {}\n</body>\n", BOOTSTRAP_JS, FONTAWESOME_JS));
    } else {
        // Last resort: append before end
        result.push('\n');
        result.push_str(BOOTSTRAP_JS);
        result.push('\n');
        result.push_str(FONTAWESOME_JS);
    }

    result
}

/// Remove all Bootstrap references from HTML.
///
/// Removes both CSS and JavaScript Bootstrap CDN links from any version.
///
/// # Arguments
///
/// * `html` - The HTML content
///
/// # Returns
///
/// HTML with Bootstrap references removed
pub fn remove_bootstrap(html: &str) -> String {
    let mut result = html.to_string();

    // Remove Bootstrap CSS references (any version)
    let bootstrap_css_regex = regex_bootstrap_css();
    result = bootstrap_css_regex.replace_all(&result, "").to_string();

    // Remove Bootstrap JS references (any version)
    let bootstrap_js_regex = regex_bootstrap_js();
    result = bootstrap_js_regex.replace_all(&result, "").to_string();

    // Remove Font Awesome CSS references (any version)
    let fontawesome_css_regex = regex_fontawesome_css();
    result = fontawesome_css_regex.replace_all(&result, "").to_string();

    // Remove Font Awesome JS references (any version)
    let fontawesome_js_regex = regex_fontawesome_js();
    result = fontawesome_js_regex.replace_all(&result, "").to_string();

    // Clean up extra whitespace from removal
    let multi_newline_regex = regex_multi_newline();
    result = multi_newline_regex.replace_all(&result, "\n").to_string();

    result.trim().to_string()
}

/// Get a regex for matching Bootstrap CSS links (any version)
fn regex_bootstrap_css() -> &'static Regex {
    static REGEX: OnceLock<Regex> = OnceLock::new();
    REGEX.get_or_init(|| {
        Regex::new(r#"<link[^>]*href="[^"]*bootstrap[^"]*\.css"[^>]*>\s*"#).unwrap()
    })
}

/// Get a regex for matching Bootstrap JS scripts (any version)
fn regex_bootstrap_js() -> &'static Regex {
    static REGEX: OnceLock<Regex> = OnceLock::new();
    REGEX.get_or_init(|| {
        Regex::new(r#"<script[^>]*src="[^"]*bootstrap[^"]*\.js"[^>]*></script>\s*"#).unwrap()
    })
}

/// Get a regex for matching Font Awesome CSS links (any version)
fn regex_fontawesome_css() -> &'static Regex {
    static REGEX: OnceLock<Regex> = OnceLock::new();
    REGEX.get_or_init(|| {
        Regex::new(r#"<link[^>]*href="[^"]*font-awesome[^"]*\.css"[^>]*>\s*"#).unwrap()
    })
}

/// Get a regex for matching Font Awesome JS scripts (any version)
fn regex_fontawesome_js() -> &'static Regex {
    static REGEX: OnceLock<Regex> = OnceLock::new();
    REGEX.get_or_init(|| {
        Regex::new(r#"<script[^>]*src="[^"]*font-awesome[^"]*\.js"[^>]*></script>\s*"#).unwrap()
    })
}

/// Get a regex for matching multiple newlines
fn regex_multi_newline() -> &'static Regex {
    static REGEX: OnceLock<Regex> = OnceLock::new();
    REGEX.get_or_init(|| Regex::new(r"\n\s*\n+").unwrap())
}

/// Get a regex for matching opening HTML tags
fn regex_open_tag() -> &'static Regex {
    static REGEX: OnceLock<Regex> = OnceLock::new();
    REGEX.get_or_init(|| Regex::new(r"<(\w+)[^>]*>").unwrap())
}

/// Get a regex for matching closing HTML tags
fn regex_close_tag() -> &'static Regex {
    static REGEX: OnceLock<Regex> = OnceLock::new();
    REGEX.get_or_init(|| Regex::new(r"</(\w+)>").unwrap())
}

/// Get a regex for matching whitespace
fn regex_whitespace() -> &'static Regex {
    static REGEX: OnceLock<Regex> = OnceLock::new();
    REGEX.get_or_init(|| Regex::new(r"\s+").unwrap())
}

/// Get a regex for matching HTML tags
fn regex_tags() -> &'static Regex {
    static REGEX: OnceLock<Regex> = OnceLock::new();
    REGEX.get_or_init(|| Regex::new(r"<[^>]+>").unwrap())
}

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

    #[test]
    fn test_escape_html() {
        let result = escape_html("<script>alert('xss')</script>");
        assert_eq!(
            result,
            "&lt;script&gt;alert(&#39;xss&#39;)&lt;/script&gt;"
        );
    }

    #[test]
    fn test_unescape_html() {
        let result = unescape_html("&lt;p&gt;Hello&lt;/p&gt;");
        assert_eq!(result, "<p>Hello</p>");
    }

    #[test]
    fn test_extract_text() {
        let result = extract_text("<p>Hello <strong>World</strong></p>");
        assert_eq!(result, "Hello World");
    }

    #[test]
    fn test_minify_html() {
        let html = "<p>  Hello   World  </p>";
        let result = minify_html(html);
        assert_eq!(result, "<p> Hello World </p>");
    }

    #[test]
    fn test_validate_html_valid() {
        let html = "<div><p>Hello</p></div>";
        assert!(validate_html(html).is_ok());
    }

    #[test]
    fn test_validate_html_mismatched() {
        let html = "<div><p>Hello</div></p>";
        assert!(validate_html(html).is_err());
    }

    #[test]
    fn test_html_builder() {
        let html = HtmlBuilder::new()
            .add_tag("p", "Hello", &[])
            .add_tag("strong", "World", &[])
            .build();
        assert!(html.contains("<p>"));
        assert!(html.contains("</p>"));
        assert!(html.contains("Hello"));
    }

    #[test]
    fn test_add_bootstrap_with_head_and_body() {
        let html = "<html><head></head><body></body></html>";
        let result = add_bootstrap(html);
        
        assert!(result.contains("bootstrap@5.3.8"));
        assert!(result.contains("bootstrap.min.css"));
        assert!(result.contains("bootstrap.bundle.min.js"));
        assert!(result.contains("</head>"));
        assert!(result.contains("</body>"));
    }

    #[test]
    fn test_add_bootstrap_replaces_old_version() {
        let html = r#"<html>
<head>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>"#;
        
        let result = add_bootstrap(html);
        
        // Should contain new version
        assert!(result.contains("bootstrap@5.3.8"));
        // Should not contain old version
        assert!(!result.contains("bootstrap@5.1.0"));
    }

    #[test]
    fn test_add_bootstrap_minimal_html() {
        let html = "<p>Hello</p>";
        let result = add_bootstrap(html);
        
        assert!(result.contains("bootstrap@5.3.8"));
        assert!(result.contains("bootstrap.min.css"));
        assert!(result.contains("bootstrap.bundle.min.js"));
    }

    #[test]
    fn test_remove_bootstrap() {
        let html = r#"<html>
<head>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
    <p>Content</p>
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>"#;
        
        let result = remove_bootstrap(html);
        
        assert!(!result.contains("bootstrap"));
        assert!(result.contains("<p>Content</p>"));
    }

    #[test]
    fn test_add_bootstrap_idempotent() {
        let html = "<html><head></head><body></body></html>";
        let result1 = add_bootstrap(html);
        let result2 = add_bootstrap(&result1);
        
        // Count bootstrap references - should be same in both
        let count1 = result1.matches("bootstrap@5.3.8").count();
        let count2 = result2.matches("bootstrap@5.3.8").count();
        
        assert_eq!(count1, count2);
    }

    #[test]
    fn test_bootstrap_css_in_head() {
        let html = "<html><head></head><body></body></html>";
        let result = add_bootstrap(html);
        
        let head_start = result.find("<head>").unwrap();
        let head_end = result.find("</head>").unwrap();
        let head_content = &result[head_start..head_end];
        
        assert!(head_content.contains("bootstrap.min.css"));
        assert!(!head_content.contains("bootstrap.bundle.min.js"));
    }

    #[test]
    fn test_bootstrap_js_in_body() {
        let html = "<html><head></head><body></body></html>";
        let result = add_bootstrap(html);
        
        let body_start = result.find("<body>").unwrap();
        let body_end = result.find("</body>").unwrap();
        let body_content = &result[body_start..body_end];
        
        assert!(body_content.contains("bootstrap.bundle.min.js"));
    }
}
