//! Runtime HTML Processor
//!
//! Processes HTML content with sanitization, metadata extraction, and
//! frontmatter parsing. Provides secure HTML processing with metadata
//! extraction and content analysis.

use std::collections::HashMap;

use anyhow::Result;
#[cfg(feature = "content-static")]
use scraper::{ElementRef, Html, Selector};
use serde::{Deserialize, Serialize};
use tracing::debug;

/// Configuration for HTML sanitization
#[derive(Debug, Clone)]
pub struct HtmlSanitizerConfig {
    pub allowed_tags: Vec<String>,
    pub allowed_attributes: HashMap<String, Vec<String>>,
    pub strip_comments: bool,
    pub clean_content_tags: bool,
    pub remove_empty_elements: bool,
}

impl Default for HtmlSanitizerConfig {
    fn default() -> Self {
        Self {
            allowed_tags: vec![
                // Text formatting
                "p".to_string(),
                "br".to_string(),
                "hr".to_string(),
                "h1".to_string(),
                "h2".to_string(),
                "h3".to_string(),
                "h4".to_string(),
                "h5".to_string(),
                "h6".to_string(),
                "strong".to_string(),
                "b".to_string(),
                "em".to_string(),
                "i".to_string(),
                "u".to_string(),
                "small".to_string(),
                "mark".to_string(),
                "del".to_string(),
                "ins".to_string(),
                "sub".to_string(),
                "sup".to_string(),
                // Links and media
                "a".to_string(),
                "img".to_string(),
                "figure".to_string(),
                "figcaption".to_string(),
                // Lists
                "ul".to_string(),
                "ol".to_string(),
                "li".to_string(),
                "dl".to_string(),
                "dt".to_string(),
                "dd".to_string(),
                // Tables
                "table".to_string(),
                "thead".to_string(),
                "tbody".to_string(),
                "tfoot".to_string(),
                "tr".to_string(),
                "th".to_string(),
                "td".to_string(),
                "caption".to_string(),
                // Quotes and code
                "blockquote".to_string(),
                "cite".to_string(),
                "q".to_string(),
                "code".to_string(),
                "pre".to_string(),
                "kbd".to_string(),
                "samp".to_string(),
                "var".to_string(),
                // Sections
                "div".to_string(),
                "span".to_string(),
                "section".to_string(),
                "article".to_string(),
                "aside".to_string(),
                "main".to_string(),
                "header".to_string(),
                "footer".to_string(),
                "nav".to_string(),
                // Details
                "details".to_string(),
                "summary".to_string(),
            ],
            allowed_attributes: {
                let mut attrs = HashMap::new();
                attrs.insert(
                    "*".to_string(),
                    vec!["id".to_string(), "class".to_string(), "title".to_string()],
                );
                attrs.insert(
                    "a".to_string(),
                    vec!["href".to_string(), "target".to_string(), "rel".to_string()],
                );
                attrs.insert(
                    "img".to_string(),
                    vec![
                        "src".to_string(),
                        "alt".to_string(),
                        "width".to_string(),
                        "height".to_string(),
                    ],
                );
                attrs.insert(
                    "th".to_string(),
                    vec![
                        "scope".to_string(),
                        "colspan".to_string(),
                        "rowspan".to_string(),
                    ],
                );
                attrs.insert(
                    "td".to_string(),
                    vec!["colspan".to_string(), "rowspan".to_string()],
                );
                attrs.insert(
                    "pre".to_string(),
                    vec!["lang".to_string(), "data-lang".to_string()],
                );
                attrs.insert(
                    "code".to_string(),
                    vec!["class".to_string(), "data-lang".to_string()],
                );
                attrs
            },
            strip_comments: true,
            clean_content_tags: true,
            remove_empty_elements: false,
        }
    }
}

/// HTML metadata extracted from document
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HtmlMetadata {
    pub title: Option<String>,
    pub description: Option<String>,
    pub keywords: Vec<String>,
    pub author: Option<String>,
    pub canonical_url: Option<String>,
    pub language: Option<String>,
    pub published_time: Option<String>,
    pub modified_time: Option<String>,
    pub article_author: Option<String>,
    pub article_section: Option<String>,
    pub article_tags: Vec<String>,
    pub custom_meta: HashMap<String, String>,
}

impl Default for HtmlMetadata {
    fn default() -> Self {
        Self {
            title: None,
            description: None,
            keywords: Vec::new(),
            author: None,
            canonical_url: None,
            language: None,
            published_time: None,
            modified_time: None,
            article_author: None,
            article_section: None,
            article_tags: Vec::new(),
            custom_meta: HashMap::new(),
        }
    }
}

/// Processed HTML content with metadata
#[derive(Debug, Clone)]
pub struct ProcessedHtml {
    pub metadata: HtmlMetadata,
    pub sanitized_html: String,
    pub raw_html: String,
    pub word_count: usize,
    pub reading_time_minutes: usize,
    pub headings: Vec<HtmlHeading>,
    pub links: Vec<HtmlLink>,
    pub images: Vec<HtmlImage>,
}

/// HTML heading for table of contents
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HtmlHeading {
    pub level: u32,
    pub title: String,
    pub id: Option<String>,
}

/// HTML link information
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HtmlLink {
    pub href: String,
    pub text: String,
    pub title: Option<String>,
    pub is_external: bool,
}

/// HTML image information
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HtmlImage {
    pub src: String,
    pub alt: String,
    pub title: Option<String>,
    pub width: Option<String>,
    pub height: Option<String>,
}

/// Runtime HTML processor with sanitization and analysis
pub struct HtmlProcessor {
    sanitizer_config: HtmlSanitizerConfig,
    extract_metadata: bool,
    analyze_content: bool,
}

impl Default for HtmlProcessor {
    fn default() -> Self {
        Self::new()
    }
}

impl HtmlProcessor {
    /// Create a new HTML processor with default configuration
    pub fn new() -> Self {
        Self {
            sanitizer_config: HtmlSanitizerConfig::default(),
            extract_metadata: true,
            analyze_content: true,
        }
    }

    /// Create HTML processor with custom sanitization config
    pub fn with_sanitizer_config(mut self, config: HtmlSanitizerConfig) -> Self {
        self.sanitizer_config = config;
        self
    }

    /// Enable or disable metadata extraction
    pub fn with_metadata_extraction(mut self, extract: bool) -> Self {
        self.extract_metadata = extract;
        self
    }

    /// Enable or disable content analysis (headings, links, images)
    pub fn with_content_analysis(mut self, analyze: bool) -> Self {
        self.analyze_content = analyze;
        self
    }

    /// Process HTML content
    #[cfg(feature = "content-static")]
    pub fn process(&self, html_content: &str) -> Result<ProcessedHtml> {
        debug!("Processing HTML content ({} bytes)", html_content.len());

        // Parse HTML
        let document = Html::parse_document(html_content);

        // Extract metadata if enabled
        let metadata = if self.extract_metadata {
            self.extract_metadata_from_html(&document)?
        } else {
            HtmlMetadata::default()
        };

        // Sanitize HTML
        let sanitized_html = self.sanitize_html(html_content)?;

        // Calculate content statistics
        let word_count = self.count_words(&sanitized_html);
        let reading_time_minutes = (word_count / 200).max(1); // Average 200 words per minute

        // Analyze content structure if enabled
        let (headings, links, images) = if self.analyze_content {
            let headings = self.extract_headings(&document)?;
            let links = self.extract_links(&document)?;
            let images = self.extract_images(&document)?;
            (headings, links, images)
        } else {
            (Vec::new(), Vec::new(), Vec::new())
        };

        Ok(ProcessedHtml {
            metadata,
            sanitized_html,
            raw_html: html_content.to_string(),
            word_count,
            reading_time_minutes,
            headings,
            links,
            images,
        })
    }

    /// Placeholder for when content-static feature is not enabled
    #[cfg(not(feature = "content-static"))]
    pub fn process(&self, html_content: &str) -> Result<ProcessedHtml> {
        Ok(ProcessedHtml {
            metadata: HtmlMetadata::default(),
            sanitized_html: html_content.to_string(),
            raw_html: html_content.to_string(),
            word_count: 0,
            reading_time_minutes: 1,
            headings: Vec::new(),
            links: Vec::new(),
            images: Vec::new(),
        })
    }

    /// Sanitize HTML content
    #[cfg(feature = "content-static")]
    fn sanitize_html(&self, html: &str) -> Result<String> {
        let mut builder = ammonia::Builder::default();

        // Configure allowed tags
        builder.tags(
            self.sanitizer_config
                .allowed_tags
                .iter()
                .map(|s| s.as_str())
                .collect(),
        );

        // Configure allowed attributes per tag
        let mut attributes_map = std::collections::HashMap::new();
        for (tag, attrs) in &self.sanitizer_config.allowed_attributes {
            if tag == "*" {
                // Global attributes
                builder.generic_attributes(attrs.iter().map(|s| s.as_str()).collect());
            } else {
                let attr_set: std::collections::HashSet<&str> =
                    attrs.iter().map(|s| s.as_str()).collect();
                attributes_map.insert(tag.as_str(), attr_set);
            }
        }
        if !attributes_map.is_empty() {
            builder.tag_attributes(attributes_map);
        }

        // Strip comments if configured
        if self.sanitizer_config.strip_comments {
            builder.strip_comments(true);
        }

        let sanitized = builder.clean(html).to_string();
        Ok(sanitized)
    }

    /// Extract metadata from HTML document
    #[cfg(feature = "content-static")]
    fn extract_metadata_from_html(&self, document: &Html) -> Result<HtmlMetadata> {
        let mut metadata = HtmlMetadata::default();

        // Extract title
        if let Ok(title_selector) = Selector::parse("title") {
            if let Some(title_elem) = document.select(&title_selector).next() {
                metadata.title = Some(
                    title_elem
                        .text()
                        .collect::<Vec<_>>()
                        .join(" ")
                        .trim()
                        .to_string(),
                );
            }
        }

        // Extract meta tags
        if let Ok(meta_selector) = Selector::parse("meta") {
            for meta_elem in document.select(&meta_selector) {
                self.process_meta_tag(&meta_elem, &mut metadata);
            }
        }

        // Extract link tags for canonical URL
        if let Ok(link_selector) = Selector::parse("link[rel=\"canonical\"]") {
            if let Some(link_elem) = document.select(&link_selector).next() {
                if let Some(href) = link_elem.value().attr("href") {
                    metadata.canonical_url = Some(href.to_string());
                }
            }
        }

        Ok(metadata)
    }

    /// Process individual meta tag
    #[cfg(feature = "content-static")]
    fn process_meta_tag(&self, meta_elem: &ElementRef, metadata: &mut HtmlMetadata) {
        let attrs = meta_elem.value();

        // Standard meta tags
        if let Some(name) = attrs.attr("name") {
            if let Some(content) = attrs.attr("content") {
                match name.to_lowercase().as_str() {
                    "description" => metadata.description = Some(content.to_string()),
                    "keywords" => {
                        metadata.keywords = content
                            .split(',')
                            .map(|k| k.trim().to_string())
                            .filter(|k| !k.is_empty())
                            .collect();
                    }
                    "author" => metadata.author = Some(content.to_string()),
                    _ => {
                        metadata
                            .custom_meta
                            .insert(name.to_string(), content.to_string());
                    }
                }
            }
        }

        // OpenGraph and Twitter meta tags
        if let Some(property) = attrs.attr("property") {
            if let Some(content) = attrs.attr("content") {
                match property {
                    "article:author" => metadata.article_author = Some(content.to_string()),
                    "article:section" => metadata.article_section = Some(content.to_string()),
                    "article:published_time" => metadata.published_time = Some(content.to_string()),
                    "article:modified_time" => metadata.modified_time = Some(content.to_string()),
                    "article:tag" => metadata.article_tags.push(content.to_string()),
                    _ => {
                        metadata
                            .custom_meta
                            .insert(property.to_string(), content.to_string());
                    }
                }
            }
        }

        // Language
        if attrs.attr("http-equiv").map(|v| v.to_lowercase())
            == Some("content-language".to_string())
        {
            if let Some(content) = attrs.attr("content") {
                metadata.language = Some(content.to_string());
            }
        }
    }

    /// Extract headings from HTML document
    #[cfg(feature = "content-static")]
    fn extract_headings(&self, document: &Html) -> Result<Vec<HtmlHeading>> {
        let mut headings = Vec::new();

        for level in 1..=6 {
            let selector_str = format!("h{}", level);
            if let Ok(selector) = Selector::parse(&selector_str) {
                for heading_elem in document.select(&selector) {
                    let title = heading_elem
                        .text()
                        .collect::<Vec<_>>()
                        .join(" ")
                        .trim()
                        .to_string();
                    let id = heading_elem.value().attr("id").map(|s| s.to_string());

                    if !title.is_empty() {
                        headings.push(HtmlHeading {
                            level: level as u32,
                            title,
                            id,
                        });
                    }
                }
            };
        }

        Ok(headings)
    }

    /// Extract links from HTML document
    #[cfg(feature = "content-static")]
    fn extract_links(&self, document: &Html) -> Result<Vec<HtmlLink>> {
        let mut links = Vec::new();

        if let Ok(link_selector) = Selector::parse("a[href]") {
            for link_elem in document.select(&link_selector) {
                if let Some(href) = link_elem.value().attr("href") {
                    let text = link_elem
                        .text()
                        .collect::<Vec<_>>()
                        .join(" ")
                        .trim()
                        .to_string();
                    let title = link_elem.value().attr("title").map(|s| s.to_string());
                    let is_external = href.starts_with("http://") || href.starts_with("https://");

                    links.push(HtmlLink {
                        href: href.to_string(),
                        text,
                        title,
                        is_external,
                    });
                }
            }
        }

        Ok(links)
    }

    /// Extract images from HTML document
    #[cfg(feature = "content-static")]
    fn extract_images(&self, document: &Html) -> Result<Vec<HtmlImage>> {
        let mut images = Vec::new();

        if let Ok(img_selector) = Selector::parse("img[src]") {
            for img_elem in document.select(&img_selector) {
                if let Some(src) = img_elem.value().attr("src") {
                    let alt = img_elem.value().attr("alt").unwrap_or("").to_string();
                    let title = img_elem.value().attr("title").map(|s| s.to_string());
                    let width = img_elem.value().attr("width").map(|s| s.to_string());
                    let height = img_elem.value().attr("height").map(|s| s.to_string());

                    images.push(HtmlImage {
                        src: src.to_string(),
                        alt,
                        title,
                        width,
                        height,
                    });
                }
            }
        }

        Ok(images)
    }

    /// Count words in HTML content (text only)
    fn count_words(&self, html: &str) -> usize {
        // Simple word counting - in a real implementation you might want to
        // use a proper HTML-to-text converter
        let text = html
            .chars()
            .map(|c| {
                if c.is_alphanumeric() || c.is_whitespace() {
                    c
                } else {
                    ' '
                }
            })
            .collect::<String>();

        text.split_whitespace()
            .filter(|word| !word.is_empty())
            .count()
    }
}

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

    #[test]
    fn test_html_processor_creation() {
        let processor = HtmlProcessor::new();
        assert!(processor.extract_metadata);
        assert!(processor.analyze_content);
    }

    #[test]
    fn test_sanitizer_config_default() {
        let config = HtmlSanitizerConfig::default();
        assert!(config.allowed_tags.contains(&"p".to_string()));
        assert!(config.allowed_tags.contains(&"a".to_string()));
        assert!(config.strip_comments);
    }

    #[test]
    fn test_word_counting() {
        let processor = HtmlProcessor::new();
        let html = "<p>This is a test paragraph with <strong>bold</strong> text.</p>";
        let count = processor.count_words(html);
        assert!(count > 5); // Should count words even with HTML tags
    }

    #[cfg(feature = "content-static")]
    #[test]
    fn test_html_processing() {
        let processor = HtmlProcessor::new();
        let html = r#"
            <!DOCTYPE html>
            <html>
            <head>
                <title>Test Document</title>
                <meta name="description" content="A test document">
                <meta name="author" content="Test Author">
            </head>
            <body>
                <h1>Main Title</h1>
                <p>This is a <strong>test</strong> paragraph.</p>
                <a href="https://example.com">External Link</a>
                <img src="test.jpg" alt="Test Image">
            </body>
            </html>
        "#;

        let result = processor.process(html).unwrap();
        assert_eq!(result.metadata.title, Some("Test Document".to_string()));
        assert_eq!(
            result.metadata.description,
            Some("A test document".to_string())
        );
        assert_eq!(result.metadata.author, Some("Test Author".to_string()));
        assert!(!result.headings.is_empty());
        assert!(!result.links.is_empty());
        assert!(!result.images.is_empty());
        assert!(result.word_count > 0);
    }
}
