//! HTML builder for server-side rendering.
//!
//! This module provides a builder-pattern API for constructing HTML.
//! It's designed to be type-safe, ergonomic, and automatically escape
//! user content to prevent XSS attacks.
//!
//! # Example
//!
//! ```
//! use soot_core::html::Html;
//!
//! let html = Html::new()
//!     .doctype()
//!     .html().children(|h| h
//!         .head().children(|h| h
//!             .title().text("My App")
//!         )
//!         .body().children(|h| h
//!             .h1().text("Hello, World!")
//!             .p().class("intro").text("Welcome to Soot.")
//!         )
//!     )
//!     .build();
//! ```

mod elements;
mod escape;

pub use escape::escape_html;

use axum::response::IntoResponse;

/// Represents rendered HTML content.
///
/// This type wraps a String containing HTML and implements `IntoResponse`
/// for easy use with Axum handlers.
#[derive(Debug, Clone, Default)]
pub struct Html(pub String);

impl Html {
    /// Create a new HTML builder.
    #[must_use]
    pub fn new() -> HtmlBuilder {
        HtmlBuilder::new()
    }

    /// Get the inner HTML string.
    #[must_use]
    pub fn into_string(self) -> String {
        self.0
    }

    /// Get a reference to the inner HTML string.
    #[must_use]
    pub fn as_str(&self) -> &str {
        &self.0
    }
}

impl IntoResponse for Html {
    fn into_response(self) -> axum::response::Response {
        axum::response::Html(self.0).into_response()
    }
}

impl std::fmt::Display for Html {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.0)
    }
}

/// Builder for constructing HTML documents.
#[derive(Debug, Default)]
pub struct HtmlBuilder {
    buffer: String,
}

impl HtmlBuilder {
    /// Create a new HTML builder.
    #[must_use]
    pub fn new() -> Self {
        Self {
            buffer: String::with_capacity(4096),
        }
    }

    /// Add the HTML5 doctype declaration.
    pub fn doctype(&mut self) -> &mut Self {
        self.buffer.push_str("<!DOCTYPE html>");
        self
    }

    /// Add raw HTML without escaping.
    ///
    /// # Safety
    ///
    /// Only use this with trusted content. User input should always
    /// go through `.text()` which escapes HTML entities.
    pub fn raw(&mut self, html: &str) -> &mut Self {
        self.buffer.push_str(html);
        self
    }

    /// Add escaped text content.
    pub fn text(&mut self, content: &str) -> &mut Self {
        self.buffer.push_str(&escape_html(content));
        self
    }

    /// Conditionally render content.
    ///
    /// # Example
    ///
    /// ```
    /// use soot_core::html::Html;
    ///
    /// let is_admin = true;
    /// let html = Html::new()
    ///     .when(is_admin, |h| h.span().class("badge").text("Admin"))
    ///     .build();
    /// ```
    pub fn when<F>(&mut self, condition: bool, f: F) -> &mut Self
    where
        F: FnOnce(&mut Self) -> &mut Self,
    {
        if condition {
            f(self);
        }
        self
    }

    /// Conditionally render one of two options.
    ///
    /// # Example
    ///
    /// ```
    /// use soot_core::html::Html;
    ///
    /// let is_logged_in = false;
    /// let html = Html::new()
    ///     .when_else(
    ///         is_logged_in,
    ///         |h| h.span().text("Welcome back!"),
    ///         |h| h.a().attr("href", "/login").text("Log in"),
    ///     )
    ///     .build();
    /// ```
    pub fn when_else<F, G>(&mut self, condition: bool, if_true: F, if_false: G) -> &mut Self
    where
        F: FnOnce(&mut Self) -> &mut Self,
        G: FnOnce(&mut Self) -> &mut Self,
    {
        if condition {
            if_true(self);
        } else {
            if_false(self);
        }
        self
    }

    /// Iterate over items and render each one.
    ///
    /// # Example
    ///
    /// ```
    /// use soot_core::html::Html;
    ///
    /// let items = vec!["Apple", "Banana", "Cherry"];
    /// let html = Html::new()
    ///     .ul().children(|h| h
    ///         .each(&items, |h, item| h.li().text(item))
    ///     )
    ///     .build();
    /// ```
    pub fn each<T, I, F>(&mut self, items: I, f: F) -> &mut Self
    where
        I: IntoIterator<Item = T>,
        F: Fn(&mut Self, T) -> &mut Self,
    {
        for item in items {
            f(self, item);
        }
        self
    }

    /// Iterate over items with index.
    ///
    /// # Example
    ///
    /// ```
    /// use soot_core::html::Html;
    ///
    /// let items = vec!["First", "Second", "Third"];
    /// let html = Html::new()
    ///     .ol().children(|h| h
    ///         .each_with_index(&items, |h, i, item| {
    ///             h.li().text(&format!("{}. {}", i + 1, item))
    ///         })
    ///     )
    ///     .build();
    /// ```
    pub fn each_with_index<T, I, F>(&mut self, items: I, f: F) -> &mut Self
    where
        I: IntoIterator<Item = T>,
        F: Fn(&mut Self, usize, T) -> &mut Self,
    {
        for (index, item) in items.into_iter().enumerate() {
            f(self, index, item);
        }
        self
    }

    /// Apply a function that may use the builder.
    ///
    /// Useful for calling component functions.
    ///
    /// # Example
    ///
    /// ```
    /// use soot_core::html::Html;
    ///
    /// let html = Html::new()
    ///     .apply(|h| h.button().class("btn").text("Click me"))
    ///     .build();
    /// ```
    pub fn apply<F>(&mut self, f: F) -> &mut Self
    where
        F: FnOnce(&mut Self) -> &mut Self,
    {
        f(self)
    }

    /// Finalize and build the HTML.
    #[must_use]
    pub fn build(&mut self) -> Html {
        Html(std::mem::take(&mut self.buffer))
    }
}

/// An HTML element being constructed.
///
/// This struct allows setting attributes before adding content or closing the element.
pub struct Element<'a> {
    builder: &'a mut HtmlBuilder,
    tag: &'static str,
    is_void: bool,
    has_opened: bool,
}

impl<'a> Element<'a> {
    pub(crate) fn new(builder: &'a mut HtmlBuilder, tag: &'static str, is_void: bool) -> Self {
        builder.buffer.push('<');
        builder.buffer.push_str(tag);
        Self {
            builder,
            tag,
            is_void,
            has_opened: false,
        }
    }

    /// Add a class attribute.
    #[must_use]
    pub fn class(self, class: &str) -> Self {
        self.attr("class", class)
    }

    /// Add an id attribute.
    #[must_use]
    pub fn id(self, id: &str) -> Self {
        self.attr("id", id)
    }

    /// Add an arbitrary attribute.
    #[must_use]
    pub fn attr(self, name: &str, value: &str) -> Self {
        self.builder.buffer.push(' ');
        self.builder.buffer.push_str(name);
        self.builder.buffer.push_str("=\"");
        self.builder.buffer.push_str(&escape_html(value));
        self.builder.buffer.push('"');
        self
    }

    /// Add a boolean attribute (present = true).
    #[must_use]
    pub fn bool_attr(self, name: &str) -> Self {
        self.builder.buffer.push(' ');
        self.builder.buffer.push_str(name);
        self
    }

    /// Conditionally add an attribute.
    #[must_use]
    pub fn attr_if(self, condition: bool, name: &str, value: &str) -> Self {
        if condition {
            self.attr(name, value)
        } else {
            self
        }
    }

    /// Add escaped text content and close the element.
    pub fn text(mut self, content: &str) -> &'a mut HtmlBuilder {
        self.open_tag();
        self.builder.buffer.push_str(&escape_html(content));
        self.close_tag();
        self.builder
    }

    /// Add raw HTML content and close the element.
    ///
    /// # Safety
    ///
    /// Only use with trusted content.
    pub fn raw(mut self, html: &str) -> &'a mut HtmlBuilder {
        self.open_tag();
        self.builder.buffer.push_str(html);
        self.close_tag();
        self.builder
    }

    /// Add child elements using a closure.
    pub fn children<F>(mut self, f: F) -> &'a mut HtmlBuilder
    where
        F: FnOnce(&mut HtmlBuilder) -> &mut HtmlBuilder,
    {
        self.open_tag();
        f(self.builder);
        self.close_tag();
        self.builder
    }

    /// Close the element without content (for void elements or empty elements).
    pub fn end(mut self) -> &'a mut HtmlBuilder {
        if self.is_void {
            self.builder.buffer.push_str(">");
        } else {
            self.open_tag();
            self.close_tag();
        }
        self.builder
    }

    fn open_tag(&mut self) {
        if !self.has_opened {
            self.builder.buffer.push('>');
            self.has_opened = true;
        }
    }

    fn close_tag(&mut self) {
        if !self.is_void {
            self.builder.buffer.push_str("</");
            self.builder.buffer.push_str(self.tag);
            self.builder.buffer.push('>');
        }
    }
}

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

    #[test]
    fn test_basic_html() {
        let html = Html::new().div().class("container").text("Hello").build();
        assert_eq!(html.as_str(), "<div class=\"container\">Hello</div>");
    }

    #[test]
    fn test_nested_elements() {
        let html = Html::new()
            .div()
            .children(|h| h.span().text("Inner"))
            .build();
        assert_eq!(html.as_str(), "<div><span>Inner</span></div>");
    }

    #[test]
    fn test_escaping() {
        let html = Html::new().div().text("<script>alert('xss')</script>").build();
        assert_eq!(
            html.as_str(),
            "<div>&lt;script&gt;alert(&#x27;xss&#x27;)&lt;/script&gt;</div>"
        );
    }

    #[test]
    fn test_when() {
        let html = Html::new()
            .when(true, |h| h.span().text("visible"))
            .when(false, |h| h.span().text("hidden"))
            .build();
        assert_eq!(html.as_str(), "<span>visible</span>");
    }

    #[test]
    fn test_each() {
        let items = vec!["a", "b", "c"];
        let html = Html::new()
            .ul()
            .children(|h| h.each(&items, |h, item| h.li().text(item)))
            .build();
        assert_eq!(html.as_str(), "<ul><li>a</li><li>b</li><li>c</li></ul>");
    }

    #[test]
    fn test_void_element() {
        let html = Html::new().input().attr("type", "text").attr("name", "email").end().build();
        assert_eq!(html.as_str(), "<input type=\"text\" name=\"email\">");
    }
}
