import typing as t
from importlib import resources
from io import StringIO

from attr import Factory
from attrs import define

from cratedb_about.util import DictTools, Dumpable, Metadata


class CrateDbKnowledgeOutline:
    """
    Load CrateDB knowledge outline from YAML file `cratedb-outline.yaml`.

    This class provides methods to read the raw YAML content and to load it
    as a structured document model.

    Examples:
        ```python
        # Get raw YAML content
        yaml_content = CrateDbKnowledgeOutline.read()

        # Load as structured document
        outline = CrateDbKnowledgeOutline.load()

        # Get all section names
        sections = outline.get_section_names()
        ```
    """

    @classmethod
    def read(cls):
        return resources.read_text("cratedb_about.outline", "cratedb-outline.yaml")

    @classmethod
    def load(cls) -> "OutlineDocument":
        return OutlineDocument.from_yaml(cls.read())


@define
class OutlineHeader(DictTools):
    """Data model element of an `OutlineDocument`"""

    title: t.Union[str, None] = None
    link: t.Union[str, None] = None
    description: str = ""


@define
class OutlineItem(DictTools):
    """Data model element of an `OutlineDocument`"""

    title: str
    link: str
    description: str


@define
class OutlineSection(DictTools):
    """Data model element of an `OutlineDocument`"""

    name: str
    items: t.List[OutlineItem] = Factory(list)


@define
class OutlineData(DictTools):
    """Data model element of an `OutlineDocument`"""

    header: OutlineHeader = Factory(OutlineHeader)
    sections: t.List[OutlineSection] = Factory(list)


ItemsOutputType = t.Union[t.List[OutlineItem], t.List[t.Dict[str, t.Any]]]


@define
class OutlineDocument(Dumpable):
    """
    Manage a set of curated knowledgebase documents.

    - It is the root element of the data model.
    - It provides conversion utility functions.
    - Also, it provides convenience functionality to query the data model.
    """

    meta: Metadata = Factory(lambda: Metadata(version=1, type="outline"))
    data: OutlineData = Factory(OutlineData)

    def to_markdown(self) -> str:
        """Convert outline into Markdown format."""
        buffer = StringIO()
        buffer.write(f"# {self.data.header.title or 'Knowledge Outline'}\n\n")
        buffer.write(f"{self.data.header.description.strip()}\n\n")
        for section in self.data.sections:
            buffer.write(f"## {section.name}\n\n")
            for item in section.items:
                buffer.write(f"- [{item.title}]({item.link}): {item.description}\n")
            buffer.write("\n")
        return buffer.getvalue().strip()

    def get_item_titles(self, section_name: t.Optional[str] = None) -> t.List[str]:
        """
        Return all item titles across all sections.

        By default, return item titles from all sections.
        When `section_name` is provided, limit search to that section.
        """
        items_in = self.collect_items(section_name=section_name)
        return [item.title for item in items_in]

    def get_section_names(self) -> t.List[str]:
        """Return all section names."""
        return [section.name for section in self.data.sections]

    def get_section(self, name: str) -> t.Optional[OutlineSection]:
        """
        Return an individual section element by its name, or `None` if not found.

        Args:
            name: The name of the section to retrieve

        Returns:
            The section if found, None otherwise

        Example:
            ```python
            outline = CrateDbKnowledgeOutline.load()
            section = outline.get_section("Getting Started")
            ```
        """
        for section in self.data.sections:
            if section.name == name:
                return section
        return None

    def get_section_safe(self, name: str) -> OutlineSection:
        """
        Return an individual section element by its name, or raise an exception if not found.
        """
        section = self.get_section(name=name)
        if not section:
            raise ValueError(
                f"Section '{name}' not found. Available sections: {self.get_section_names()}"
            )
        return section

    def find_items(
        self,
        title: t.Optional[str] = None,
        section_name: t.Optional[str] = None,
        as_dict: bool = False,
    ) -> ItemsOutputType:
        """
        Find `OutlineItem` elements by their titles, per lower-cased "contains" search.

        By default, return all titles from all sections.
        When `title` is provided, search for matching titles.
        When `section_name` is provided, limit search to that section.
        When no item can be found, return an empty list.
        """
        items_in = self.collect_items(section_name=section_name)
        items_out = []
        needle = None
        if title:
            needle = title.casefold()
        for item in items_in:
            if not needle or needle in item.title.casefold():
                items_out.append(item)
        return self.output_items(items=items_out, as_dict=as_dict)

    def collect_items(self, section_name: t.Optional[str] = None) -> t.List[OutlineItem]:
        """
        Return the list of `OutlineItem` elements, optionally filtered by `section_name`.
        """
        items = []
        if section_name is None:
            for section in self.data.sections:
                items += section.items
        else:
            section_ = self.get_section_safe(name=section_name)
            items += section_.items
        return items

    @staticmethod
    def output_items(items: t.List[OutlineItem], as_dict: bool = False) -> ItemsOutputType:
        """
        Return the list of `OutlineItem` elements either as objects or as dictionaries.
        """
        if as_dict:
            return [item.to_dict() for item in items]
        return items
