"""Module with HTML generators."""

from __future__ import annotations

from collections.abc import Iterable
from typing import Optional, Union

import soupsavvy.exceptions as exc
from soupsavvy.testing.generators import namespace
from soupsavvy.testing.generators.base import BaseGenerator
from soupsavvy.testing.generators.templates.base import BaseTemplate
from soupsavvy.testing.generators.templates.config import (
    DEFAULT_TEXT_TEMPLATE,
    DEFAULT_VALUE_TEMPLATE,
)
from soupsavvy.testing.generators.templates.templates import ConstantTemplate

# types for generators
TemplateType = Optional[Union[BaseTemplate, str]]

AttributeType = Union["AttributeGenerator", str, tuple[str, TemplateType]]
ChildType = Union["TagGenerator", str]

AttributeList = Iterable[AttributeType]
ChildList = Iterable[ChildType]

AttributeGeneratorInitExceptions = (
    TypeError,
    exc.InvalidTemplateException,
    exc.EmptyNameException,
)


def _get_template_type(
    value: TemplateType, param: str, default: BaseTemplate
) -> BaseTemplate:
    """
    Get the template type from the input value.

    Parameters
    ----------
    value : TemplateType
        The value to get the template type from.
    param : str
        The name of the parameter.
    default : BaseTemplate
        The default template to use if the value is None.

    Returns
    -------
    BaseTemplate
        The template instance.

    Raises
    ------
    InvalidTemplateException
        If the value is not a string, BaseTemplate or None.
    """
    if value is None:
        return default
    elif isinstance(value, str):
        return ConstantTemplate(value)
    elif not isinstance(value, BaseTemplate):
        raise exc.InvalidTemplateException(
            f"{param} must be a string or a BaseTemplate, not {type(value)}"
        )

    return value


class AttributeGenerator(BaseGenerator):
    """
    Class for generating HTML attribute strings.

    AttributeGenerator allows to generate empty HTML attribute with specific name:

    Example
    --------
    >>> gen = AttributeGenerator("class")
    >>> gen.generate()
    'class=""'

    Attribute can also be generated with specified constant value:

    Example
    --------
    >>> gen = AttributeGenerator("class", value="container")
    >>> gen.generate()
    'class="container"'

    Value can be passed as BaseTemplate instance as well:

    Example
    --------
    >>> from soupsavvy.testing.generators import RandomTemplate
    >>> template = RandomTemplate(length=4, seed=42)
    >>> gen = AttributeGenerator("id", value=template)
    >>> gen.generate()
    'id="Nbrn"'

    For more information on available Templates, how to use them
    and customize for your needs, see the documentation.

    See also
    --------
    soupsavvy.testing.generators.templates module
    soupsavvy.testing.generators.TagGenerator class
    """

    def __init__(self, name: str, value: TemplateType = None) -> None:
        """
        Initializes the AttributeGenerator.

        Parameters
        ----------
        name : str
            The name of the attribute.
        value : TemplateType, optional
            The value of the attribute. Defaults to None.
        """
        self._check_name(name)
        self.name = name
        self.value = _get_template_type(
            value,
            param="name",
            default=DEFAULT_VALUE_TEMPLATE,
        )

    def _check_name(self, name: str) -> None:
        """
        Check if the input name is a valid HTML attribute name.

        Parameters
        ----------
        name : str
            The name to check.

        Raises
        ------
        TypeError
            If the name is not a string.
        EmptyNameException
            If the name is an empty string.
        """
        if not isinstance(name, str):
            raise TypeError(f"'name' parameter must be a string, not {type(name)}")

        if not name:
            raise exc.EmptyNameException(
                "Empty string is not allowed html attribute name, "
                "not-empty string must be provided for 'name' parameter."
            )

    def generate(self) -> str:
        """
        Generates the HTML attribute string.

        Returns
        -------
        str
            The generated HTML attribute string.
        """
        value = self.value.generate()
        return f'{self.name}="{value}"'


class TagGenerator(BaseGenerator):
    """
    Class for generating HTML tag strings.

    TagGenerator allows to generate empty HTML tag with specific name:

    Example
    --------
    >>> gen = TagGenerator("div")
    >>> gen.generate()
    '<div></div>'

    Tag can also be generated with specified attributes:

    Example
    --------
    >>> gen = TagGenerator("div", attrs=[("class", "container")])
    >>> gen.generate()
    '<div class="container"></div>'

    Attributes can be passed as an iterable of of mixed types:
    * AttributeGenerator - instance of AttributeGenerator class
    * str - attribute name, value would be a default Template (empty string)
    * tuple[str, TemplateType] - attribute name and literal value or Template

    Example
    --------
    >>> gen = TagGenerator(
    >>>     name="a",
    >>>     attrs=[
    >>>         ("id", "link"),
    >>>         AttributeGenerator("href", "/endpoint"),
    >>>         "class",
    >>>     ],
    >>> )
    >>> gen.generate()
    '<div id="link" href="/endpoint", class=""></div>'

    Similarly, children can be passed as an iterable of mixed types:
    * TagGenerator - instance of TagGenerator class
    * str - tag name, children tags would be empty

    Example
    --------
    >>> gen = TagGenerator(
    >>>     name="div",
    >>>     children=[
    >>>         "a",
    >>>         TagGenerator("span", attrs=[("class", "container")],
    >>>     ],
    >>> )
    >>> gen.generate()
    '<div><a></a><span class="container"></span></div>'

    Text of the tag can be passed as a string or a Template:

    Example
    --------
    >>> gen = TagGenerator("p", text="Hello, World!")
    >>> gen.generate()
    '<p>Hello, World!</p>'

    Example
    --------
    >>> from soupsavvy.testing.generators import RandomTemplate
    >>> template = RandomTemplate(length=4, seed=42)
    >>> gen = TagGenerator("p", text=template)
    >>> gen.generate()
    '<p>Nbrn</p>'

    For more information on available Templates, how to use them
    and customize for your needs, see the documentation.

    Void tags like <img>, <br>, <hr> etc. can be generated as well
    and are automatically closed:

    Example
    --------
    >>> gen = TagGenerator("img", attrs=[("src", "/path/to/image.jpg")])
    >>> gen.generate()
    '<img src="/path/to/image.jpg"/>'

    No children are allowed for void tags, and an error will be raised.

    See also
    --------
    soupsavvy.testing.generators.templates module
    soupsavvy.testing.generators.AttributeGenerator class
    """

    def __init__(
        self,
        name: str,
        attrs: AttributeList = (),
        children: ChildList = (),
        text: TemplateType = None,
    ) -> None:
        """
        Initialize the TagGenerator.

        Parameters
        ----------
        name : str
            The name of the HTML tag.
        attrs : AttributeList, optional
            The attributes of the tag. Defaults to empty tuple.
        children : ChildList, optional
            The children of the tag. Defaults to empty tuple.
        text : TemplateType, optional
            The text content of the tag.
            Defaults to None, which generates empty string.
        """
        if isinstance(attrs, str):
            raise TypeError("'attrs' must be an iterable of attributes, not a string")

        self._void = name in namespace.VOID_TAGS

        if self._void and children:
            raise exc.VoidTagWithChildrenException(
                f"'{name}' is a void tag and cannot have children"
            )

        self._check_name(name)
        self.name = name

        self.text = _get_template_type(
            text,
            param="text",
            default=DEFAULT_TEXT_TEMPLATE,
        )

        self.attributes = self._process_attributes(attrs)
        self.children = [
            child if isinstance(child, TagGenerator) else TagGenerator(child)
            for child in children
        ]

    def _check_name(self, name: str) -> None:
        """
        Check if the input name is a valid HTML tag name.

        Parameters
        ----------
        name : str
            The name to check.

        Raises
        ------
        TypeError
            If the name is not a string.
        EmptyNameException
            If the name is an empty string.
        """
        if not isinstance(name, str):
            raise TypeError(f"'name' parameter must be a string, not {type(name)}")

        if not name:
            raise exc.EmptyNameException(
                "Empty string is not allowed html tag name, "
                "not-empty string must be provided for 'name' parameter."
            )

    def _process_attributes(
        self, attributes: AttributeList
    ) -> list[AttributeGenerator]:
        """
        Process the input attributes of the tag.

        Parameters
        ----------
        attributes : AttributeList
            The attributes to process.

        Returns
        -------
        list[AttributeGenerator]
            The processed instances of AttributeGenerators.

        Raises
        ------
        AttributeParsingError
            If the input attributes could not be parsed into AttributeGenerators.
        """
        # dict approach for uniqueness check
        attr_dict: dict[str, AttributeGenerator] = {}

        for attr in attributes:
            if isinstance(attr, str):
                attr = (attr, None)

            if not isinstance(attr, AttributeGenerator):
                try:
                    attr = AttributeGenerator(*attr)
                except AttributeGeneratorInitExceptions as e:
                    raise exc.AttributeParsingError(
                        f"Attribute {attr} could not be parsed into AttributeGenerator "
                        "due to following error:"
                    ) from e

            attr_name = attr.name

            if attr_name in attr_dict:
                raise exc.NotUniqueAttributesException(
                    f"Input attribute {attr_name} is not unique."
                )

            attr_dict[attr_name] = attr

        return list(attr_dict.values())

    def generate(self) -> str:
        """
        Generates the HTML tag string.

        Returns
        -------
        str
            The generated HTML tag string.
        """
        attrs = " ".join(attr.generate() for attr in self.attributes)
        sep = " " if attrs else ""
        children = "".join(child.generate() for child in self.children)
        tag_content = f"{self.name}{sep}{attrs}"

        if self._void:
            return f"<{tag_content}/>"

        text = self.text.generate()
        return f"<{tag_content}>{children}{text}</{self.name}>"
