from abc import ABC
import re
from typing import Optional, Any


from bs4.element import PageElement


from bs4 import BeautifulSoup
from bs4.element import Tag, NavigableString
from pydantic import BaseModel, computed_field


from trame.config import BS4_HTML_PARSER

from trame.piece.consolidate import clean_text_nodes


class Actor(BaseModel, ABC): ...


class ListElement(Actor): ...


class Piece(BaseModel, ABC):
    page_element_bs4: Any  # recursion error else
    # page_element_string: str
    # html_from_md: str

    @computed_field
    @property
    def page_element_tag(self) -> str:
        if not hasattr(self, "_page_element_tag_cache"):
            self._page_element_tag_cache = str(self.page_element_bs4.name)
        return self._page_element_tag_cache

    @computed_field
    @property
    def page_element_string(self) -> str:
        if not hasattr(self, "_page_element_string_cache"):
            self._page_element_string_cache = str(self.page_element_bs4)
        return self._page_element_string_cache

    @computed_field
    @property
    def html(self) -> str:
        if not hasattr(self, "_html_cache"):
            self._html_cache = clean_text_nodes(self.page_element_bs4)
        return self._html_cache

    # @computed_field
    # @property
    # def html_from_md(self) -> str:
    #     return str(self.page_element_bs4)

    # # NOTE: not a computed_field as we save html as a string, we don't need it
    # # NOTE: sel: no name soup
    # # when we serialize, only at runtime
    # @property
    # def soup(self) -> PageElement:
    #     document = BeautifulSoup(self.html_from_md, features="html.parser")
    #     return next(document.children)

    # NOTE: needs to be overwritten if any actors for a given concrete class

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


class Paragraph(Piece): ...


class Title(Piece):
    @computed_field
    @property
    def level(self) -> int:
        m = re.match(r"h(?P<level>[1-3])", self.page_element_bs4.name)
        return int(m.group("level"))


class UnorderedList(Piece):
    @computed_field
    @property
    def actors(self) -> list[Actor]:
        return list(
            ListElement(html=str(element)) for element in self.page_element_bs4.find_all("li")
        )


class Code(Piece):
    @computed_field
    @property
    def language(self) -> Optional[str]:
        # 1. Trouve le tag <code> (pas <pre>!)
        code_tag = self.page_element_bs4.find("code")

        if code_tag is None:
            return None

        # 2. get('class') retourne une LISTE
        classes = code_tag.get("class", [])

        if not classes:
            return None

        # 3. Itère sur la liste
        for cls in classes:
            if isinstance(cls, str) and cls.startswith("language-"):
                return cls.replace("language-", "", 1)

        return None


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]


# Piece shouldn't know about soup, only one specific HTML element (from parsing)


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
        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:
            return Code(page_element_bs4=element)
        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)
        # elif concrete_cls is None:
        #     return None
        else:
            raise NotImplementedError(f"{type(element)=} - {concrete_cls=} - {str(element)=}")

        # if isinstance(element, Tag):
        #     html_from_md = str(element)
        #     concrete_cls = Piece.html_tag_to_concrete_cls(element)
        #     return concrete_cls(html_from_md=html_from_md)
        # elif isinstance(element, NavigableString) and element.strip() == "":
        #     return None
        # else:
        #     raise NotImplementedError(f"{type(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=}")

    # @staticmethod
    # def clean_text_nodes(element: PageElement) -> str:
    #     """Recursively clean \n from text nodes only, preserve HTML structure."""
    #     if isinstance(element, NavigableString):
    #         return str(element).replace("\n", " ")

    #     # Clone and clean recursively
    #     cleaned = BeautifulSoup(str(element), features=BS4_HTML_PARSER)
    #     for text_node in cleaned.find_all(string=True):
    #         text_node.replace_with(text_node.replace("\n", " "))

    #     # next(cleaned.children) gets the first (and typically only) child element
    #     # this is the actual HTML element we care about (e.g., the <p> tag)
    #     return str(next(cleaned.children))


######################################################################
