from abc import ABC
import re

# from functools import cached_property
from typing import Optional, OrderedDict

from bs4.element import Tag, NavigableString, PageElement
from pydantic import BaseModel, ConfigDict
import strictyaml

from trame.piece.consolidate import clean_text_nodes


class Actor(BaseModel, ABC): ...


class Piece(BaseModel, ABC):
    model_config = ConfigDict(arbitrary_types_allowed=True)
    page_element_bs4: PageElement
    page_element_tag: str = None
    page_element_string: str = None
    html: str = None
    actors: Optional[list[Actor]] = None

    def model_post_init(self, context):
        # NOTE mad: this avoids decorator and caching spaghetti
        # and raises error early as we can
        super().model_post_init(context)
        # The tag name of the parsed html element
        self.page_element_tag = str(self.page_element_bs4.name)
        # The complete string of the parsed html element
        self.page_element_string = str(self.page_element_bs4)
        # Clean html we want to display
        self.html = clean_text_nodes(self.page_element_bs4)

    @classmethod
    def build_from_bs4_element(cls, element: PageElement) -> "Piece":
        return PieceBuilder.from_bs4_element(element)


#####################################################################
# CONCRETE ACTORS
#####################################################################


class ListElement(Actor): ...


class YamlContent(Actor):
    yaml_str: str
    data: OrderedDict = None
    pca_nature: str = None

    def model_post_init(self, context):
        yaml = strictyaml.load(self.yaml_str)

        self.data = yaml.data
        self.pca_nature = self.data.pop("pca_nature", "pure_yaml")


#####################################################################
# CONCRETE PIECES
#####################################################################


class Paragraph(Piece): ...


class Title(Piece):
    level: int = None

    def model_post_init(self, context):
        super().model_post_init(context)
        m = re.match(r"h(?P<level>[1-3])", self.page_element_tag)
        self.level = int(m.group("level"))


class UnorderedList(Piece):
    def model_post_init(self, context):
        super().model_post_init(context)
        self.actors = list(
            ListElement(html=str(element)) for element in self.page_element_bs4.find_all("li")
        )


class Code(Piece):
    language: Optional[str] = None
    code_str: str = None
    content: Actor = None

    def model_post_init(self, context):
        super().model_post_init(context)

        code_tag = self.page_element_bs4.find("code")

        # NOTE: decode_contents for python unicode, encode for utf8 encoded bytestring
        self.code_str = code_tag.decode_contents()

        self.language = None
        classes = code_tag.get("class", [])
        if classes:
            for cls in classes:
                if isinstance(cls, str) and cls.startswith("language-"):
                    self.language = cls.replace("language-", "", 1)
                    break


class YamlCode(Code):
    def model_post_init(self, context):
        super().model_post_init(context)
        self.content = YamlContent(yaml_str=self.code_str)
        self.actors = [self.content]


class Table(Piece): ...


class HRule(Piece): ...


class Div(Piece): ...


class EmptyParagraph(Piece): ...


class EmptyNavigableString: ...


class NoTagName: ...


# NOTE : en utilisant la position on en a pas besoin
# car pas besoin de faire des groupes arbitraires
# class VGroup(Piece):
#     pieces: list[Piece]


class PieceBuilder:
    @staticmethod
    def from_bs4_element(element: PageElement) -> Piece:
        """Build a Piece from a BeautifulSoup element (FROM HTML=MARKDOWN PARSED)."""

        concrete_cls = PieceBuilder.html_tag_to_concrete_cls(element)

        if concrete_cls is Paragraph:
            # Check if paragraph is truly empty (no text content at all)
            # element.get_text(strip=True) returns all text content, handling nested elements
            text_content = element.get_text(strip=True)
            if text_content == "":
                return None
            else:
                return Paragraph(page_element_bs4=element)

        # Only for empty navigable strings
        # TODO mad clean, use contrete_cls
        if concrete_cls is EmptyNavigableString:
            return None
        elif concrete_cls is NoTagName:
            return None
        elif concrete_cls is Title:
            return Title(page_element_bs4=element)
        elif concrete_cls is Paragraph:
            return Paragraph(page_element_bs4=element)
        elif concrete_cls is UnorderedList:
            return UnorderedList(page_element_bs4=element)
        elif concrete_cls is Code:
            code = Code(page_element_bs4=element)
            if code.language == "yaml":
                return YamlCode(page_element_bs4=element)
            return code
        elif concrete_cls is Table:
            return Table(page_element_bs4=element)
        elif concrete_cls is HRule:
            return HRule(page_element_bs4=element)
        elif concrete_cls is Div:
            return Div(page_element_bs4=element)

        else:
            raise NotImplementedError(f"{type(element)=} - {concrete_cls=} - {str(element)=}")

    @staticmethod
    def html_tag_to_concrete_cls(tag: Tag) -> type:
        # Check for NavigableString first

        if isinstance(tag, NavigableString):
            if tag.strip() == "":
                return EmptyNavigableString
            else:
                raise NotImplementedError(f"Non-empty NavigableString: {tag!r}")

        # Check if tag has a name attribute
        elif not hasattr(tag, "name") or tag.name is None:
            return NoTagName

        # Match tag names
        if tag.name == "p":
            return Paragraph
        elif tag.name == "ul":
            return UnorderedList
        elif tag.name == "pre":
            return Code
        elif tag.name == "table":
            return Table
        elif tag.name == "hr":
            return HRule
        elif tag.name == "div":
            return Div
        elif re.match(r"^h[1-3]$", tag.name):
            return Title
        else:
            raise NotImplementedError(f"{tag.name=}")
