//! HTML output consumer that converts Wasot events to HTML.
//!
//! This module provides `HtmlListener`, a consumer that outputs HTML to stdout
//! based on Wasot element events. It maps Wasot block types to appropriate
//! HTML elements and handles proper escaping.

use crate::events::{Consumer, Event};
use crate::parsers::wasot::MetaLine;
use std::io::{self, Write};

/// HTML5 void elements that cannot have content and don't need closing tags.
#[allow(dead_code)]
const HTML_VOID_ELEMENTS: &[&str] = &[
    "area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "source", "track",
    "wbr",
];

/// Simple HTML listener that prints HTML to stdout based on ElementEvents.
///
/// This consumer converts Wasot events to HTML output, mapping Wasot block types
/// to appropriate HTML elements and handling proper escaping of content and attributes.
pub struct HtmlListener {
    /// Whether to use pretty printing with indentation
    pretty_print: bool,
    /// Current indentation level
    indent_level: usize,
    /// Whether we're inside a pre/code block (preserve whitespace)
    in_pre_block: bool,
    /// Stack of element names for proper closing
    element_stack: Vec<String>,
}

impl HtmlListener {
    /// Create a new HtmlListener.
    ///
    /// # Arguments
    /// * `pretty_print` - Whether to format output with indentation and newlines
    pub fn new(pretty_print: bool) -> Self {
        HtmlListener {
            pretty_print,
            indent_level: 0,
            in_pre_block: false,
            element_stack: Vec::new(),
        }
    }

    /// Write indentation if pretty printing is enabled and not in pre block.
    fn write_indent(&self) {
        if self.pretty_print && !self.in_pre_block {
            print!("{}", "  ".repeat(self.indent_level));
        }
    }

    /// Write a newline if pretty printing is enabled and not in pre block.
    fn write_newline(&self) {
        if self.pretty_print && !self.in_pre_block {
            println!();
        }
    }

    /// Escape text content for HTML.
    ///
    /// Escapes `&`, `<`, and `>` characters.
    fn escape_html(text: &str) -> String {
        html_escape::encode_text(text).into_owned()
    }

    /// Escape attribute value for HTML.
    ///
    /// Escapes `&`, `<`, `>`, `"`, and `'` characters.
    fn escape_attribute(text: &str) -> String {
        html_escape::encode_double_quoted_attribute(text).into_owned()
    }

    /// Check if an HTML element is a void element (self-closing).
    #[allow(dead_code)]
    fn is_void_html_element(name: &str) -> bool {
        HTML_VOID_ELEMENTS.contains(&name.to_lowercase().as_str())
    }

    /// Parse markdown-style heading from type name.
    /// Returns heading level (1-6) if the type is a `#`-prefixed heading.
    fn parse_heading_level(wasot_type: &str) -> Option<u8> {
        let trimmed = wasot_type.trim();
        if trimmed.is_empty() || !trimmed.starts_with('#') {
            return None;
        }

        // Count consecutive # characters
        let hash_count = trimmed.chars().take_while(|c| *c == '#').count();

        // Heading levels 1-6 only
        if (1..=6).contains(&hash_count) {
            Some(hash_count as u8)
        } else {
            None
        }
    }

    /// Map a Wasot block type to an HTML element name.
    ///
    /// Returns the HTML element name and optionally a custom type for x-type attribute
    /// if the block type doesn't have a direct HTML equivalent.
    fn map_block_type(wasot_type: &str) -> (&'static str, Option<&str>) {
        // First check for markdown-style headings (# = h1, ## = h2, etc.)
        if let Some(level) = Self::parse_heading_level(wasot_type) {
            return match level {
                1 => ("h1", None),
                2 => ("h2", None),
                3 => ("h3", None),
                4 => ("h4", None),
                5 => ("h5", None),
                _ => ("h6", None),
            };
        }

        match wasot_type.to_lowercase().as_str() {
            // Direct mappings
            "p" | "paragraph" => ("p", None),
            "h1" => ("h1", None),
            "h2" => ("h2", None),
            "h3" => ("h3", None),
            "h4" => ("h4", None),
            "h5" => ("h5", None),
            "h6" => ("h6", None),
            "div" => ("div", None),
            "span" => ("span", None),
            "section" => ("section", None),
            "article" => ("article", None),
            "header" => ("header", None),
            "footer" => ("footer", None),
            "nav" => ("nav", None),
            "aside" => ("aside", None),
            "main" => ("main", None),
            "blockquote" | "quote" => ("blockquote", None),
            "pre" => ("pre", None),
            "code" => ("code", None),
            "ul" => ("ul", None),
            "ol" => ("ol", None),
            "li" | "item" => ("li", None),
            "dl" => ("dl", None),
            "dt" => ("dt", None),
            "dd" => ("dd", None),
            "table" => ("table", None),
            "thead" => ("thead", None),
            "tbody" => ("tbody", None),
            "tfoot" => ("tfoot", None),
            "tr" => ("tr", None),
            "th" => ("th", None),
            "td" => ("td", None),
            "figure" => ("figure", None),
            "figcaption" => ("figcaption", None),
            "a" | "link" => ("a", None),
            "em" | "emphasis" => ("em", None),
            "strong" | "bold" => ("strong", None),
            "small" => ("small", None),
            "mark" => ("mark", None),
            "del" | "deleted" => ("del", None),
            "ins" | "inserted" => ("ins", None),
            "sub" => ("sub", None),
            "sup" => ("sup", None),
            "details" => ("details", None),
            "summary" => ("summary", None),
            "form" => ("form", None),
            "label" => ("label", None),
            "button" => ("button", None),
            "select" => ("select", None),
            "option" => ("option", None),
            "textarea" => ("textarea", None),
            "fieldset" => ("fieldset", None),
            "legend" => ("legend", None),
            // Void elements
            "br" => ("br", None),
            "hr" => ("hr", None),
            "img" | "image" => ("img", None),
            "input" => ("input", None),
            "meta" => ("meta", None),
            "area" => ("area", None),
            "base" => ("base", None),
            "col" => ("col", None),
            "embed" => ("embed", None),
            "source" => ("source", None),
            "track" => ("track", None),
            "wbr" => ("wbr", None),
            // Generic fallback - use div with class
            _ => ("div", Some(wasot_type)),
        }
    }

    /// Write the opening tag for an element with attributes.
    fn write_opening_tag(&mut self, meta_line: &MetaLine, is_void: bool) {
        let (element_name, custom_type) = Self::map_block_type(&meta_line.type_name);

        self.write_indent();
        print!("<{element_name}");

        // Add x-type attribute for custom block types
        if let Some(type_name) = custom_type {
            print!(" x-type=\"{}\"", Self::escape_attribute(type_name));
        }

        // Add id if present
        if !meta_line.id.value.is_empty() {
            print!(" id=\"{}\"", Self::escape_attribute(&meta_line.id.value));
        }

        // Process kwargs into attributes
        for (key, value) in &meta_line.arguments.kwargs {
            if key.content != "id" {
                // id is already handled above
                print!(
                    " {}=\"{}\"",
                    Self::escape_attribute(&key.content),
                    Self::escape_attribute(&value.content)
                );
            }
        }

        // For void elements, don't add closing bracket yet if we have inline text
        if is_void {
            // Inline text goes into alt attribute for img, or content attribute for meta
            if !meta_line.inline_text.content.is_empty() {
                match element_name {
                    "img" => {
                        print!(
                            " alt=\"{}\"",
                            Self::escape_attribute(&meta_line.inline_text.content)
                        );
                    }
                    "meta" => {
                        print!(
                            " content=\"{}\"",
                            Self::escape_attribute(&meta_line.inline_text.content)
                        );
                    }
                    "input" => {
                        print!(
                            " value=\"{}\"",
                            Self::escape_attribute(&meta_line.inline_text.content)
                        );
                    }
                    _ => {
                        // For other void elements, inline text becomes a data attribute
                        print!(
                            " data-content=\"{}\"",
                            Self::escape_attribute(&meta_line.inline_text.content)
                        );
                    }
                }
            }
            print!(">");
        } else {
            print!(">");

            // Write inline text if present (for non-void elements)
            if !meta_line.inline_text.content.is_empty() {
                print!("{}", Self::escape_html(&meta_line.inline_text.content));
            }
        }

        self.write_newline();

        // Track element for closing
        if !is_void {
            self.element_stack.push(element_name.to_string());
            self.indent_level += 1;

            // Track if we're entering a pre/code block
            if element_name == "pre" || element_name == "code" {
                self.in_pre_block = true;
            }
        }

        io::stdout().flush().unwrap();
    }

    /// Process a single event.
    fn process_event(&mut self, event: &Event) {
        match event {
            Event::BlockStarted { meta_line } => {
                self.write_opening_tag(meta_line, false);
            }

            Event::TextFinished { lines, .. } => {
                if lines.is_empty() {
                    return;
                }

                for (i, line) in lines.iter().enumerate() {
                    if !self.in_pre_block {
                        self.write_indent();
                    }
                    print!("{}", Self::escape_html(line));
                    if i < lines.len() - 1 || self.in_pre_block {
                        println!();
                    } else {
                        self.write_newline();
                    }
                }

                io::stdout().flush().unwrap();
            }

            Event::BlockFinished => {
                if let Some(element_name) = self.element_stack.pop() {
                    // Check if we're leaving a pre/code block
                    if element_name == "pre" || element_name == "code" {
                        self.in_pre_block = false;
                    }

                    self.indent_level = self.indent_level.saturating_sub(1);
                    self.write_indent();
                    print!("</{element_name}>");
                    self.write_newline();

                    io::stdout().flush().unwrap();
                }
            }

            Event::ContextLine { meta_line } => {
                // Output context lines as HTML comments
                self.write_indent();
                print!("<!-- context: {}", meta_line.type_name);
                if !meta_line.id.value.is_empty() {
                    print!(" id={}", meta_line.id.value);
                }
                if !meta_line.inline_text.content.is_empty() {
                    print!(" \"{}\"", meta_line.inline_text.content);
                }
                print!(" -->");
                self.write_newline();

                io::stdout().flush().unwrap();
            }

            Event::VoidElement { meta_line } => {
                self.write_opening_tag(meta_line, true);
            }
        }
    }
}

impl Consumer for HtmlListener {
    fn handle(&mut self, event: Event) {
        self.process_event(&event);
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::parsers::wasot::arguments::ParsedArguments;
    use crate::parsers::wasot::{Id, QuotedString};

    // Helper to create a test MetaLine
    #[allow(clippy::type_complexity)]
    fn create_meta_line(
        type_name: &str,
        level: usize,
        inline_text: &str,
        id: &str,
        args: Vec<(&str, bool)>,
        kwargs: Vec<((&str, bool), (&str, bool))>,
    ) -> MetaLine {
        let args: Vec<QuotedString> = args
            .into_iter()
            .map(|(s, q)| QuotedString::new(s.to_string(), q))
            .collect();
        let kwargs: Vec<(QuotedString, QuotedString)> = kwargs
            .into_iter()
            .map(|((k, kq), (v, vq))| {
                (
                    QuotedString::new(k.to_string(), kq),
                    QuotedString::new(v.to_string(), vq),
                )
            })
            .collect();

        MetaLine {
            type_name: type_name.to_string(),
            level,
            inline_text: QuotedString::new(inline_text.to_string(), false),
            id: if id.is_empty() {
                Id::empty()
            } else {
                Id::new(id.to_string())
            },
            line_index: Some(0),
            is_void: false,
            arguments: ParsedArguments { args, kwargs },
        }
    }

    #[test]
    fn test_new() {
        let listener = HtmlListener::new(true);
        assert!(listener.pretty_print);
        assert_eq!(listener.indent_level, 0);
        assert!(!listener.in_pre_block);
        assert!(listener.element_stack.is_empty());

        let listener = HtmlListener::new(false);
        assert!(!listener.pretty_print);
    }

    #[test]
    fn test_escape_html() {
        assert_eq!(HtmlListener::escape_html("hello"), "hello");
        assert_eq!(HtmlListener::escape_html("<script>"), "&lt;script&gt;");
        assert_eq!(HtmlListener::escape_html("a & b"), "a &amp; b");
        // Note: encode_text doesn't escape quotes as they're safe in text content
        assert_eq!(
            HtmlListener::escape_html("<a href=\"x\">"),
            "&lt;a href=\"x\"&gt;"
        );
        assert_eq!(HtmlListener::escape_html(""), "");
    }

    #[test]
    fn test_escape_attribute() {
        assert_eq!(HtmlListener::escape_attribute("hello"), "hello");
        assert_eq!(
            HtmlListener::escape_attribute("a \"quote\""),
            "a &quot;quote&quot;"
        );
        assert_eq!(HtmlListener::escape_attribute("a & b"), "a &amp; b");
        assert_eq!(HtmlListener::escape_attribute(""), "");
    }

    #[test]
    fn test_is_void_html_element() {
        // Test all void elements
        assert!(HtmlListener::is_void_html_element("br"));
        assert!(HtmlListener::is_void_html_element("hr"));
        assert!(HtmlListener::is_void_html_element("img"));
        assert!(HtmlListener::is_void_html_element("input"));
        assert!(HtmlListener::is_void_html_element("meta"));
        assert!(HtmlListener::is_void_html_element("link"));
        assert!(HtmlListener::is_void_html_element("area"));
        assert!(HtmlListener::is_void_html_element("base"));
        assert!(HtmlListener::is_void_html_element("col"));
        assert!(HtmlListener::is_void_html_element("embed"));
        assert!(HtmlListener::is_void_html_element("source"));
        assert!(HtmlListener::is_void_html_element("track"));
        assert!(HtmlListener::is_void_html_element("wbr"));

        // Case insensitive
        assert!(HtmlListener::is_void_html_element("BR"));
        assert!(HtmlListener::is_void_html_element("Img"));

        // Non-void elements
        assert!(!HtmlListener::is_void_html_element("div"));
        assert!(!HtmlListener::is_void_html_element("p"));
        assert!(!HtmlListener::is_void_html_element("span"));
    }

    #[test]
    fn test_map_block_type_direct_mappings() {
        assert_eq!(HtmlListener::map_block_type("p"), ("p", None));
        assert_eq!(HtmlListener::map_block_type("paragraph"), ("p", None));
        assert_eq!(HtmlListener::map_block_type("h1"), ("h1", None));
        assert_eq!(HtmlListener::map_block_type("h2"), ("h2", None));
        assert_eq!(HtmlListener::map_block_type("h3"), ("h3", None));
        assert_eq!(HtmlListener::map_block_type("h4"), ("h4", None));
        assert_eq!(HtmlListener::map_block_type("h5"), ("h5", None));
        assert_eq!(HtmlListener::map_block_type("h6"), ("h6", None));
        assert_eq!(HtmlListener::map_block_type("div"), ("div", None));
        assert_eq!(HtmlListener::map_block_type("section"), ("section", None));
        assert_eq!(HtmlListener::map_block_type("article"), ("article", None));
        assert_eq!(
            HtmlListener::map_block_type("blockquote"),
            ("blockquote", None)
        );
        assert_eq!(HtmlListener::map_block_type("quote"), ("blockquote", None));
        assert_eq!(HtmlListener::map_block_type("ul"), ("ul", None));
        assert_eq!(HtmlListener::map_block_type("ol"), ("ol", None));
        assert_eq!(HtmlListener::map_block_type("li"), ("li", None));
        assert_eq!(HtmlListener::map_block_type("item"), ("li", None));
    }

    #[test]
    fn test_map_block_type_void_elements() {
        assert_eq!(HtmlListener::map_block_type("br"), ("br", None));
        assert_eq!(HtmlListener::map_block_type("hr"), ("hr", None));
        assert_eq!(HtmlListener::map_block_type("img"), ("img", None));
        assert_eq!(HtmlListener::map_block_type("image"), ("img", None));
        assert_eq!(HtmlListener::map_block_type("input"), ("input", None));
    }

    #[test]
    fn test_map_block_type_case_insensitive() {
        assert_eq!(HtmlListener::map_block_type("P"), ("p", None));
        assert_eq!(HtmlListener::map_block_type("DIV"), ("div", None));
        assert_eq!(HtmlListener::map_block_type("Section"), ("section", None));
    }

    #[test]
    fn test_map_block_type_generic_fallback() {
        assert_eq!(
            HtmlListener::map_block_type("custom-block"),
            ("div", Some("custom-block"))
        );
        assert_eq!(
            HtmlListener::map_block_type("my-component"),
            ("div", Some("my-component"))
        );
    }

    #[test]
    fn test_process_block_started() {
        let mut listener = HtmlListener::new(false);
        let meta_line = create_meta_line("p", 1, "", "", vec![], vec![]);
        let event = Event::BlockStarted { meta_line };

        listener.process_event(&event);

        assert_eq!(listener.indent_level, 1);
        assert_eq!(listener.element_stack, vec!["p"]);
    }

    #[test]
    fn test_process_block_started_with_id() {
        let mut listener = HtmlListener::new(false);
        let meta_line = create_meta_line("div", 1, "", "my-id", vec![], vec![]);
        let event = Event::BlockStarted { meta_line };

        listener.process_event(&event);

        assert_eq!(listener.element_stack, vec!["div"]);
    }

    #[test]
    fn test_process_block_started_with_class_kwarg() {
        let mut listener = HtmlListener::new(false);
        let meta_line = create_meta_line(
            "div",
            1,
            "",
            "",
            vec![],
            vec![(("class", false), ("highlight", false))],
        );
        let event = Event::BlockStarted { meta_line };

        listener.process_event(&event);

        assert_eq!(listener.element_stack, vec!["div"]);
    }

    #[test]
    fn test_process_block_started_with_inline_text() {
        let mut listener = HtmlListener::new(false);
        let meta_line = create_meta_line("h1", 1, "Hello World", "", vec![], vec![]);
        let event = Event::BlockStarted { meta_line };

        listener.process_event(&event);

        assert_eq!(listener.element_stack, vec!["h1"]);
    }

    #[test]
    fn test_process_block_finished() {
        let mut listener = HtmlListener::new(false);

        // First start a block
        let meta_line = create_meta_line("p", 1, "", "", vec![], vec![]);
        listener.process_event(&Event::BlockStarted { meta_line });

        assert_eq!(listener.indent_level, 1);
        assert_eq!(listener.element_stack.len(), 1);

        // Then finish it
        listener.process_event(&Event::BlockFinished);

        assert_eq!(listener.indent_level, 0);
        assert!(listener.element_stack.is_empty());
    }

    #[test]
    fn test_process_text_finished() {
        let mut listener = HtmlListener::new(false);

        let event = Event::TextFinished {
            lines: vec!["Line 1".to_string(), "Line 2".to_string()],
            links: vec![vec![], vec![]],
            footnotes: vec![],
        };

        listener.process_event(&event);
        // Just verify it doesn't panic
    }

    #[test]
    fn test_process_text_finished_empty() {
        let mut listener = HtmlListener::new(false);

        let event = Event::TextFinished {
            lines: vec![],
            links: vec![],
            footnotes: vec![],
        };

        listener.process_event(&event);
        // Should handle empty lines gracefully
    }

    #[test]
    fn test_process_void_element() {
        let mut listener = HtmlListener::new(false);
        let meta_line = create_meta_line("hr", 1, "", "", vec![], vec![]);
        let event = Event::VoidElement {
            meta_line: MetaLine {
                is_void: true,
                ..meta_line
            },
        };

        listener.process_event(&event);

        // Void elements shouldn't be pushed to stack
        assert!(listener.element_stack.is_empty());
        assert_eq!(listener.indent_level, 0);
    }

    #[test]
    fn test_process_void_element_with_inline_text() {
        let mut listener = HtmlListener::new(false);
        let meta_line = create_meta_line("img", 1, "Alt text", "", vec![], vec![]);
        let event = Event::VoidElement {
            meta_line: MetaLine {
                is_void: true,
                ..meta_line
            },
        };

        listener.process_event(&event);

        assert!(listener.element_stack.is_empty());
    }

    #[test]
    fn test_process_void_element_with_attrs() {
        let mut listener = HtmlListener::new(false);
        let meta_line = create_meta_line(
            "img",
            1,
            "Alt text",
            "",
            vec![],
            vec![(("src", false), ("image.jpg", false))],
        );
        let event = Event::VoidElement {
            meta_line: MetaLine {
                is_void: true,
                ..meta_line
            },
        };

        listener.process_event(&event);

        assert!(listener.element_stack.is_empty());
    }

    #[test]
    fn test_process_context_line() {
        let mut listener = HtmlListener::new(false);
        let meta_line = create_meta_line("context-type", 1, "Some context", "", vec![], vec![]);
        let event = Event::ContextLine { meta_line };

        listener.process_event(&event);
        // Just verify it doesn't panic
    }

    #[test]
    fn test_pre_block_tracking() {
        let mut listener = HtmlListener::new(true);

        // Enter pre block
        let meta_line = create_meta_line("pre", 1, "", "", vec![], vec![]);
        listener.process_event(&Event::BlockStarted { meta_line });

        assert!(listener.in_pre_block);

        // Exit pre block
        listener.process_event(&Event::BlockFinished);

        assert!(!listener.in_pre_block);
    }

    #[test]
    fn test_nested_blocks() {
        let mut listener = HtmlListener::new(false);

        // Start article
        let meta_line = create_meta_line("article", 1, "", "", vec![], vec![]);
        listener.process_event(&Event::BlockStarted { meta_line });
        assert_eq!(listener.element_stack, vec!["article"]);
        assert_eq!(listener.indent_level, 1);

        // Start section
        let meta_line = create_meta_line("section", 2, "", "", vec![], vec![]);
        listener.process_event(&Event::BlockStarted { meta_line });
        assert_eq!(listener.element_stack, vec!["article", "section"]);
        assert_eq!(listener.indent_level, 2);

        // Start p
        let meta_line = create_meta_line("p", 3, "", "", vec![], vec![]);
        listener.process_event(&Event::BlockStarted { meta_line });
        assert_eq!(listener.element_stack, vec!["article", "section", "p"]);
        assert_eq!(listener.indent_level, 3);

        // Close p
        listener.process_event(&Event::BlockFinished);
        assert_eq!(listener.element_stack, vec!["article", "section"]);
        assert_eq!(listener.indent_level, 2);

        // Close section
        listener.process_event(&Event::BlockFinished);
        assert_eq!(listener.element_stack, vec!["article"]);
        assert_eq!(listener.indent_level, 1);

        // Close article
        listener.process_event(&Event::BlockFinished);
        assert!(listener.element_stack.is_empty());
        assert_eq!(listener.indent_level, 0);
    }

    #[test]
    fn test_consumer_trait_handle() {
        let mut listener = HtmlListener::new(false);
        let meta_line = create_meta_line("p", 1, "", "", vec![], vec![]);
        let event = Event::BlockStarted { meta_line };

        listener.handle(event);

        assert_eq!(listener.indent_level, 1);
    }

    #[test]
    fn test_generic_block_gets_x_type() {
        let (element, custom_type) = HtmlListener::map_block_type("custom-widget");
        assert_eq!(element, "div");
        assert_eq!(custom_type, Some("custom-widget"));
    }

    #[test]
    fn test_parse_heading_level() {
        // Valid markdown headings
        assert_eq!(HtmlListener::parse_heading_level("#"), Some(1));
        assert_eq!(HtmlListener::parse_heading_level("##"), Some(2));
        assert_eq!(HtmlListener::parse_heading_level("###"), Some(3));
        assert_eq!(HtmlListener::parse_heading_level("####"), Some(4));
        assert_eq!(HtmlListener::parse_heading_level("#####"), Some(5));
        assert_eq!(HtmlListener::parse_heading_level("######"), Some(6));

        // Too many hashes - still h6
        assert_eq!(HtmlListener::parse_heading_level("#######"), None);

        // With whitespace
        assert_eq!(HtmlListener::parse_heading_level("  ##"), Some(2));

        // Not headings
        assert_eq!(HtmlListener::parse_heading_level("p"), None);
        assert_eq!(HtmlListener::parse_heading_level("h1"), None);
        assert_eq!(HtmlListener::parse_heading_level(""), None);
    }

    #[test]
    fn test_markdown_heading_mapping() {
        assert_eq!(HtmlListener::map_block_type("#"), ("h1", None));
        assert_eq!(HtmlListener::map_block_type("##"), ("h2", None));
        assert_eq!(HtmlListener::map_block_type("###"), ("h3", None));
        assert_eq!(HtmlListener::map_block_type("####"), ("h4", None));
        assert_eq!(HtmlListener::map_block_type("#####"), ("h5", None));
        assert_eq!(HtmlListener::map_block_type("######"), ("h6", None));
    }
}
