//! HTML input emitter that parses HTML and emits Wasot events.
//!
//! This module provides `HtmlEmitter`, an emitter that parses HTML documents
//! and converts them to Wasot events. It maps HTML elements to appropriate
//! Wasot block types.

use crate::context::Context;
use crate::events::events::Emitter;
use crate::events::{BusReaderIterator, Event};
use crate::parsers::wasot::arguments::ParsedArguments;
use crate::parsers::wasot::{Id, MetaLine, QuotedString};
use bus::Bus;
use scraper::{Html, Node};
use std::fs;
use std::io::{self, Read};
use std::path::Path;
use std::sync::{Arc, Mutex};

/// HTML5 void elements that cannot have content.
const HTML_VOID_ELEMENTS: &[&str] = &[
    "area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "source", "track",
    "wbr",
];

/// Configuration for the HTML emitter.
#[derive(Debug, Clone, Default)]
pub struct HtmlEmitterConfig {
    /// Extract main content from the page (look for main, article, etc.)
    pub extract_main_content: bool,
    /// Strip script elements
    pub strip_scripts: bool,
    /// Strip style elements
    pub strip_styles: bool,
    /// Strip nav elements
    pub strip_nav: bool,
    /// Strip header elements
    pub strip_header: bool,
    /// Strip footer elements
    pub strip_footer: bool,
}

impl HtmlEmitterConfig {
    /// Create a new config with all stripping enabled
    pub fn clean() -> Self {
        Self {
            extract_main_content: true,
            strip_scripts: true,
            strip_styles: true,
            strip_nav: true,
            strip_header: false,
            strip_footer: false,
        }
    }
}

/// Emitter that parses HTML and emits Wasot events.
pub struct HtmlEmitter {
    /// HTML content to parse
    content: String,
    /// Bus for events
    bus: Arc<Mutex<Bus<Event>>>,
    /// Configuration options
    config: HtmlEmitterConfig,
    /// Next ID to assign to blocks
    next_auto_id: u64,
}

impl HtmlEmitter {
    /// Create a new HTML emitter from a file path.
    pub fn from_file<P: AsRef<Path>>(file_path: P) -> io::Result<Self> {
        let content = fs::read_to_string(file_path)?;
        Ok(Self::from_string(content))
    }

    /// Create a new HTML emitter from a string.
    pub fn from_string(content: String) -> Self {
        let bus = Arc::new(Mutex::new(Bus::new(1000)));

        Self {
            content,
            bus,
            config: HtmlEmitterConfig::default(),
            next_auto_id: 1,
        }
    }

    /// Create a new HTML emitter from a reader.
    pub fn from_reader<R: Read>(mut reader: R) -> io::Result<Self> {
        let mut content = String::new();
        reader.read_to_string(&mut content)?;
        Ok(Self::from_string(content))
    }

    /// Set the configuration for this emitter.
    pub fn with_config(mut self, config: HtmlEmitterConfig) -> Self {
        self.config = config;
        self
    }

    /// Check if an element should be stripped based on config.
    fn should_strip(&self, element_name: &str) -> bool {
        match element_name.to_lowercase().as_str() {
            "script" => self.config.strip_scripts,
            "style" => self.config.strip_styles,
            "nav" => self.config.strip_nav,
            "header" => self.config.strip_header,
            "footer" => self.config.strip_footer,
            _ => false,
        }
    }

    /// Check if an element is a void element.
    fn is_void_element(name: &str) -> bool {
        HTML_VOID_ELEMENTS.contains(&name.to_lowercase().as_str())
    }

    /// Map HTML element name to Wasot block type.
    /// If x_type is provided (from x-type attribute), use that instead.
    fn map_element_type(html_element: &str, x_type: Option<&str>) -> String {
        // If there's an x-type attribute, use that as the block type
        if let Some(custom_type) = x_type {
            return custom_type.to_string();
        }

        match html_element.to_lowercase().as_str() {
            // Map headings to markdown-style # syntax
            "h1" => "#".to_string(),
            "h2" => "##".to_string(),
            "h3" => "###".to_string(),
            "h4" => "####".to_string(),
            "h5" => "#####".to_string(),
            "h6" => "######".to_string(),
            // Keep most HTML elements as-is since they map directly
            "p" | "div" | "span" | "section" | "article" | "header" | "footer" | "nav"
            | "aside" | "main" | "ul" | "ol" | "li" | "dl" | "dt" | "dd" | "table" | "thead"
            | "tbody" | "tfoot" | "tr" | "th" | "td" | "figure" | "figcaption" | "blockquote"
            | "pre" | "code" | "em" | "strong" | "small" | "mark" | "del" | "ins" | "sub"
            | "sup" | "a" | "details" | "summary" | "form" | "label" | "button" | "select"
            | "option" | "textarea" | "fieldset" | "legend" | "br" | "hr" | "img" | "input"
            | "meta" | "link" | "area" | "base" | "col" | "embed" | "source" | "track" | "wbr" => {
                html_element.to_lowercase()
            }
            // Map some HTML elements to semantic Wasot types
            "b" => "strong".to_string(),
            "i" => "em".to_string(),
            "u" => "ins".to_string(),
            "s" => "del".to_string(),
            // Default: keep the element name
            _ => html_element.to_lowercase(),
        }
    }

    /// Generate the next auto ID.
    fn next_id(&mut self) -> String {
        let id = self.next_auto_id.to_string();
        self.next_auto_id += 1;
        id
    }

    /// Broadcast an event to subscribers.
    fn broadcast(&self, event: Event) {
        if let Ok(mut bus) = self.bus.lock() {
            bus.broadcast(event);
        }
    }

    /// Process an element node and its children.
    fn process_element(&mut self, element: scraper::ElementRef, level: usize) {
        let element_name = element.value().name();

        // Check if we should strip this element
        if self.should_strip(element_name) {
            return;
        }

        // Build kwargs from attributes, extracting special ones
        let mut kwargs: Vec<(QuotedString, QuotedString)> = Vec::new();
        let mut element_id = String::new();
        let mut x_type: Option<String> = None;

        for (attr_name, attr_value) in element.value().attrs() {
            if attr_name == "id" {
                element_id = attr_value.to_string();
            } else if attr_name == "x-type" {
                // Use x-type as the block type
                x_type = Some(attr_value.to_string());
            } else {
                kwargs.push((
                    QuotedString::new(attr_name.to_string(), false),
                    QuotedString::new(attr_value.to_string(), true),
                ));
            }
        }

        let type_name = Self::map_element_type(element_name, x_type.as_deref());
        let is_void = Self::is_void_element(element_name);

        // Generate ID if not present
        if element_id.is_empty() {
            element_id = self.next_id();
        }

        // Create MetaLine
        let meta_line = MetaLine {
            type_name,
            level,
            inline_text: QuotedString::new(String::new(), false),
            id: Id::new(element_id),
            line_index: None,
            is_void,
            arguments: ParsedArguments {
                args: Vec::new(),
                kwargs,
            },
        };

        // Emit appropriate event
        if is_void {
            self.broadcast(Event::VoidElement { meta_line });
        } else {
            self.broadcast(Event::BlockStarted { meta_line });

            // Process children
            for child in element.children() {
                match child.value() {
                    Node::Text(text) => {
                        let text_content = text.text.trim();
                        if !text_content.is_empty() {
                            // Split into lines
                            let lines: Vec<String> =
                                text_content.lines().map(|s| s.to_string()).collect();
                            let links = vec![vec![]; lines.len()];
                            self.broadcast(Event::TextFinished {
                                lines,
                                links,
                                footnotes: vec![],
                            });
                        }
                    }
                    Node::Element(_) => {
                        if let Some(child_element) = scraper::ElementRef::wrap(child) {
                            self.process_element(child_element, level + 1);
                        }
                    }
                    _ => {
                        // Ignore comments, doctypes, etc.
                    }
                }
            }

            // Emit BlockFinished
            self.broadcast(Event::BlockFinished);
        }
    }

    /// Find the main content element if extract_main_content is enabled.
    fn find_main_content<'a>(&self, document: &'a Html) -> Option<scraper::ElementRef<'a>> {
        if !self.config.extract_main_content {
            return None;
        }

        // Try various selectors for main content
        let selectors = [
            "main",
            "article",
            "[role=\"main\"]",
            ".content",
            ".main-content",
            "#content",
            "#main",
        ];

        for selector_str in selectors {
            if let Ok(selector) = scraper::Selector::parse(selector_str) {
                if let Some(element) = document.select(&selector).next() {
                    return Some(element);
                }
            }
        }

        None
    }

    /// Parse and process the HTML content.
    fn process_content(&mut self) {
        let document = Html::parse_document(&self.content);

        // Try to find main content or use body
        if let Some(main_element) = self.find_main_content(&document) {
            self.process_element(main_element, 1);
        } else if let Ok(body_selector) = scraper::Selector::parse("body") {
            if let Some(body) = document.select(&body_selector).next() {
                // Process body's direct children
                for child in body.children() {
                    if let Some(element) = scraper::ElementRef::wrap(child) {
                        self.process_element(element, 1);
                    }
                }
            }
        } else {
            // Fallback: process html element
            if let Ok(html_selector) = scraper::Selector::parse("html") {
                if let Some(html_elem) = document.select(&html_selector).next() {
                    self.process_element(html_elem, 1);
                }
            }
        }
    }
}

impl Emitter for HtmlEmitter {
    type Reader = BusReaderIterator;

    fn subscribe(&self) -> Self::Reader {
        let bus_reader = self.bus.lock().unwrap().add_rx();
        BusReaderIterator::new(bus_reader)
    }

    fn run(mut self) {
        self.process_content();
    }

    fn run_with_context(mut self, _context: &mut Context) {
        self.process_content();
    }
}

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

    #[test]
    fn test_from_string() {
        let emitter = HtmlEmitter::from_string("<p>Hello</p>".to_string());
        assert!(!emitter.content.is_empty());
    }

    #[test]
    fn test_is_void_element() {
        assert!(HtmlEmitter::is_void_element("br"));
        assert!(HtmlEmitter::is_void_element("hr"));
        assert!(HtmlEmitter::is_void_element("img"));
        assert!(HtmlEmitter::is_void_element("BR")); // case insensitive
        assert!(!HtmlEmitter::is_void_element("div"));
        assert!(!HtmlEmitter::is_void_element("p"));
    }

    #[test]
    fn test_map_element_type() {
        assert_eq!(HtmlEmitter::map_element_type("p", None), "p");
        assert_eq!(HtmlEmitter::map_element_type("DIV", None), "div");
        assert_eq!(HtmlEmitter::map_element_type("b", None), "strong");
        assert_eq!(HtmlEmitter::map_element_type("i", None), "em");
        assert_eq!(
            HtmlEmitter::map_element_type("custom-element", None),
            "custom-element"
        );
    }

    #[test]
    fn test_map_element_type_headings() {
        // Headings should map to markdown-style #
        assert_eq!(HtmlEmitter::map_element_type("h1", None), "#");
        assert_eq!(HtmlEmitter::map_element_type("h2", None), "##");
        assert_eq!(HtmlEmitter::map_element_type("h3", None), "###");
        assert_eq!(HtmlEmitter::map_element_type("h4", None), "####");
        assert_eq!(HtmlEmitter::map_element_type("h5", None), "#####");
        assert_eq!(HtmlEmitter::map_element_type("h6", None), "######");
    }

    #[test]
    fn test_map_element_type_with_x_type() {
        // x-type should override element name
        assert_eq!(
            HtmlEmitter::map_element_type("div", Some("custom-widget")),
            "custom-widget"
        );
        assert_eq!(
            HtmlEmitter::map_element_type("span", Some("my-component")),
            "my-component"
        );
    }

    #[test]
    fn test_config_clean() {
        let config = HtmlEmitterConfig::clean();
        assert!(config.extract_main_content);
        assert!(config.strip_scripts);
        assert!(config.strip_styles);
        assert!(config.strip_nav);
    }

    #[test]
    fn test_should_strip() {
        let emitter =
            HtmlEmitter::from_string("".to_string()).with_config(HtmlEmitterConfig::clean());

        assert!(emitter.should_strip("script"));
        assert!(emitter.should_strip("style"));
        assert!(emitter.should_strip("nav"));
        assert!(!emitter.should_strip("div"));
        assert!(!emitter.should_strip("p"));
    }

    #[test]
    fn test_simple_paragraph() {
        let emitter = HtmlEmitter::from_string("<p>Hello World</p>".to_string());
        let receiver = emitter.subscribe();

        std::thread::spawn(move || {
            emitter.run();
        });

        let events: Vec<Event> = receiver.collect();

        // Should have: BlockStarted, TextFinished, BlockFinished
        assert!(events.len() >= 2); // At minimum BlockStarted and BlockFinished

        // Check first event is BlockStarted with type "p"
        if let Event::BlockStarted { meta_line } = &events[0] {
            assert_eq!(meta_line.type_name, "p");
        } else {
            panic!("First event should be BlockStarted");
        }
    }

    #[test]
    fn test_void_element() {
        let emitter = HtmlEmitter::from_string("<br>".to_string());
        let receiver = emitter.subscribe();

        std::thread::spawn(move || {
            emitter.run();
        });

        let events: Vec<Event> = receiver.collect();

        // Should have a VoidElement event
        let has_void = events
            .iter()
            .any(|e| matches!(e, Event::VoidElement { .. }));
        assert!(has_void, "Should have a VoidElement event");
    }

    #[test]
    fn test_nested_elements() {
        let emitter = HtmlEmitter::from_string(
            "<article><section><p>Text</p></section></article>".to_string(),
        );
        let receiver = emitter.subscribe();

        std::thread::spawn(move || {
            emitter.run();
        });

        let events: Vec<Event> = receiver.collect();

        // Count BlockStarted and BlockFinished events
        let starts = events
            .iter()
            .filter(|e| matches!(e, Event::BlockStarted { .. }))
            .count();
        let finishes = events
            .iter()
            .filter(|e| matches!(e, Event::BlockFinished))
            .count();

        assert_eq!(
            starts, finishes,
            "BlockStarted and BlockFinished should match"
        );
        assert!(starts >= 3, "Should have at least 3 nested elements");
    }

    #[test]
    fn test_attributes_preserved() {
        let emitter = HtmlEmitter::from_string(
            r#"<div id="my-id" class="highlight" data-value="42">Content</div>"#.to_string(),
        );
        let receiver = emitter.subscribe();

        std::thread::spawn(move || {
            emitter.run();
        });

        let events: Vec<Event> = receiver.collect();

        if let Event::BlockStarted { meta_line } = &events[0] {
            assert_eq!(meta_line.id.value, "my-id");

            // Check kwargs contain class and data-value
            let has_class = meta_line
                .arguments
                .kwargs
                .iter()
                .any(|(k, v)| k.content == "class" && v.content == "highlight");
            let has_data = meta_line
                .arguments
                .kwargs
                .iter()
                .any(|(k, v)| k.content == "data-value" && v.content == "42");

            assert!(has_class, "Should have class attribute");
            assert!(has_data, "Should have data-value attribute");
        } else {
            panic!("First event should be BlockStarted");
        }
    }

    #[test]
    fn test_strip_scripts() {
        let emitter = HtmlEmitter::from_string(
            "<div><script>alert('hi')</script><p>Keep me</p></div>".to_string(),
        )
        .with_config(HtmlEmitterConfig::clean());

        let receiver = emitter.subscribe();

        std::thread::spawn(move || {
            emitter.run();
        });

        let events: Vec<Event> = receiver.collect();

        // Should not have any script blocks
        let has_script = events.iter().any(|e| {
            if let Event::BlockStarted { meta_line } = e {
                meta_line.type_name == "script"
            } else {
                false
            }
        });

        assert!(!has_script, "Script elements should be stripped");
    }

    #[test]
    fn test_text_content() {
        let emitter = HtmlEmitter::from_string("<p>Line 1</p>".to_string());
        let receiver = emitter.subscribe();

        std::thread::spawn(move || {
            emitter.run();
        });

        let events: Vec<Event> = receiver.collect();

        let has_text = events.iter().any(|e| {
            if let Event::TextFinished { lines, .. } = e {
                lines.iter().any(|l| l.contains("Line 1"))
            } else {
                false
            }
        });

        assert!(has_text, "Should have text content");
    }

    #[test]
    fn test_img_with_attributes() {
        let emitter =
            HtmlEmitter::from_string(r#"<img src="photo.jpg" alt="A photo">"#.to_string());
        let receiver = emitter.subscribe();

        std::thread::spawn(move || {
            emitter.run();
        });

        let events: Vec<Event> = receiver.collect();

        let has_img = events.iter().any(|e| {
            if let Event::VoidElement { meta_line } = e {
                meta_line.type_name == "img"
                    && meta_line
                        .arguments
                        .kwargs
                        .iter()
                        .any(|(k, _)| k.content == "src")
            } else {
                false
            }
        });

        assert!(has_img, "Should have img void element with src");
    }
}
