"""Module to convert Report to a Word document."""

import re
from typing import Any, cast

import latex2mathml.converter
import mathml2omml
import numpy as np
from docx import Document
from docx.document import Document as DocumentObject
from docx.enum.text import WD_PARAGRAPH_ALIGNMENT
from docx.oxml import OxmlElement, parse_xml
from docx.oxml.ns import qn
from docx.oxml.xmlchemy import BaseOxmlElement
from docx.shared import Inches, Pt, RGBColor
from docx.text.paragraph import Paragraph


class _ReportToWordConverter:
    r"""
    Converter to transform a LaTeX report (with text blocks, equations, titles, sections, tables) to a Word Document.

    Please note: this feature is not intended to be a full LaTeX to Word converter, but rather to handle
    the specific LaTeX constructs generated by the Report class. It is therefore recommended to
    use the Report class to create the LaTeX content, converted with a .to_latex() or .to_word() method, for best compatibility.

    Limitations and Usage Notes:
    - Text blocks using \text{...} (can also be \textbf{...} and/or \textit{...} for bold/italic).
    - Equations in \begin{equation}...\end{equation} environments (with optional \tag{...}).
    - Titles, sections, subsections, and subsubsections using \title{...}, \section{...}, \subsection{...}, \subsubsection{...}.
    - Tables in \begin{table}...\end{table} environments with tabular content
    - Figures in \begin{figure}...\end{figure} environments with \includegraphics.
    - Bullet lists in \begin{itemize}...\end{itemize} environments with \item for each bullet.
    - Numbered lists in \begin{enumerate}...\end{enumerate} environments with \item for each item.
    - Inline equations (not in equation environments) between $...$.
    - New lines indicated by \newline.
    - Uses styles from the template.

    Examples
    --------
    Basic usage with automatic conversion:

    >>> from blueprints.utils.report import Report
    >>> report = Report("My Report Title")
    >>> report.add_heading("Introduction")
    >>> report.add_paragraph("This is some text.")
    >>> latex_content = report.to_latex()
    >>> converter = _ReportToWordConverter(latex_content)
    >>> converter.document.save("my_report.docx")

    Using the convenience method from Report:

    >>> report = Report("My Report Title")
    >>> report.add_heading("Introduction")
    >>> report.add_paragraph("This is some text.")
    >>> report.to_word("my_report.docx")

    Comprehensive example showing all supported features:

    >>> from blueprints.utils.report import Report
    >>> report = Report("Testing Title")
    >>> report.add_heading("Testing Section")
    >>> report.add_heading("Testing Subsection", level=2)
    >>> report.add_heading("Testing Subsubsection", level=3)
    >>> report.add_paragraph("Bold and", bold=True).add_paragraph(" normal text.").add_newline(n=6)
    >>> report.add_paragraph("And Italic text.", italic=True).add_paragraph(" And also bold and italic.", bold=True, italic=True).add_newline()
    >>> report.add_paragraph("Here is an inline equation: $E=mc^2$ within the text.").add_newline()
    >>> report.add_paragraph("test").add_equation("E=mc^2", tag="1", inline=True).add_paragraph("more text.").add_newline()
    >>> report.add_paragraph("test").add_equation("E=mc^2", inline=True).add_paragraph("more text.").add_newline()
    >>> report.add_equation("E=mc^2", tag="4")
    >>> report.add_equation(r"\int_a^b f(x)dx = F(b) - F(a)")
    >>> report.add_list(["One", ["A", "B", "C"], "Two", ["A", ["I", "II", ["A", "B"], "III"]]], style="numbered")
    >>> report.add_list(["First", "Second", ["Subfirst", "Subsecond"], "Third"], style="bulleted")
    >>> report.add_paragraph("Here is a table:")
    >>> report.add_table(
    ...     headers=["Header 1", "Header 2", "Header 3 with math $E=mc^2$"],
    ...     rows=[["Row 1 Col 1", "Row 1 Col 2 with inline math $a^2 + b^2 = c^2$", "Row 1 Col 3"], ["Row 2 Col 1", "Row 2 Col 2", "Row 2 Col 3"]],
    ... )
    >>> report.add_heading("Another Section")
    >>> report.add_heading("Another Subsection", level=2)
    >>> report.add_heading("Yet another Subsection", level=2)
    >>> report.add_heading("Yet another Subsubsection", level=3)
    >>> report.add_heading("Yet another Subsubsection v2", level=3)
    >>> report.to_word("test_report.docx", language="zh")  # Convert to Word document in Chinese

    """

    def __init__(self, content: str | None = None) -> None:
        """Initialize the converter and optionally convert content.

        Args:
            content: Optional LaTeX content to convert immediately. If provided,
                     the document will be available via the .document attribute.
        """
        self.template_docx: str | None = None
        self.section_counter: int = 0
        self.subsection_counter: int = 0
        self.subsubsection_counter: int = 0
        self.document: DocumentObject | None = None

        if content is not None:
            self.document = self._to_word(content)

    def _to_word(self, content: str) -> DocumentObject:
        r"""
        Convert a LaTeX string (with text, equations, titles, sections, subsections, tables) to a Word Document object.
        Args:
            content: The report to convert.

        Returns
        -------
            The python-docx Document object (not saved).
        """
        # Create a new Document
        doc = Document()

        # Convert content to string
        content_str = str(content)

        # Preprocess to remove LaTeX preamble commands
        content_str = self._preprocess_content(content_str)
        matches = self._extract_structural_elements(content_str)
        self._add_content_to_document(doc, matches)
        return doc

    @staticmethod
    def _preprocess_content(content: str) -> str:
        """Remove LaTeX preamble and document structure commands that should be ignored.

        Args:
            content: The raw LaTeX string.

        Returns
        -------
            Cleaned content with preamble and document structure removed
        """
        # Remove everything before and including \begin{document}
        begin_match = re.search(r"\\begin\{document\}", content)
        if begin_match:
            content = content[begin_match.end() :]

        # Remove everything after and including \end{document}
        end_match = re.search(r"\\end\{document\}", content)
        if end_match:
            content = content[: end_match.start()]

        # Remove \date{...} commands
        content = re.sub(r"\\date\{.*?\}", "", content)

        # Remove \maketitle commands
        content = re.sub(r"\\maketitle", "", content)

        return content.strip()

    @staticmethod
    def _extract_structural_elements(content: str) -> list[dict[str, str | int]]:
        """Extract structural elements (titles, sections, tables) from LaTeX content.

        Args:
            content: The LaTeX string to parse.

        Returns
        -------
            A list of dictionaries containing type, content, start, and end positions.
        """
        matches: list[dict[str, str | int]] = []

        # Define patterns for structural elements
        start_patterns = {
            "title": r"\\title\{",
            "section": r"\\section\{",
            "subsection": r"\\subsection\{",
            "subsubsection": r"\\subsubsection\{",
            "table": r"\\begin\{table\}",
            "figure": r"\\begin\{figure\}",
            "itemize": r"\\begin\{itemize\}",
            "enumerate": r"\\begin\{enumerate\}",
            "equation": r"\\begin\{equation\}",
            "newline": r"\\newline",
        }

        # Extract matches for each pattern
        current_pos = 0
        for line in content.splitlines(keepends=True):
            # Special handling for newlines - find all occurrences in the line
            newline_matches = list(re.finditer(r"\\newline", line))
            if newline_matches:
                matches.extend(
                    {
                        "type": "newline",
                        "content": "",
                        "start": current_pos + match.start(),
                        "end": current_pos + match.end(),
                    }
                    for match in newline_matches
                )
                current_pos += len(line)
                continue

            # Check other patterns
            for category, pattern in start_patterns.items():
                if category != "newline" and re.match(pattern, line):
                    matches.append(
                        {
                            "type": category,
                            "content": line,
                            "start": current_pos,
                            "end": current_pos + len(line),
                        }
                    )
                    break
            else:
                if len(line.strip()) > 0:
                    matches.append({"type": "text", "content": line, "start": current_pos, "end": current_pos + len(line)})

            current_pos += len(line)

        return matches

    def _add_content_to_document(self, doc: DocumentObject, parsed: list[dict[str, str | int]]) -> None:
        """Add parsed content items to the Word document.

        Args:
            doc: The Word Document object to add content to.
            parsed: List of parsed content items.
        """
        i = 0  # Index to track position in parsed list
        while i < len(parsed):
            item = parsed[i]
            content = str(item["content"]).strip()
            item_type = str(item["type"])
            if item_type == "text":
                # Group consecutive text items into a single paragraph
                i = self._add_grouped_text(doc, parsed, i)
            elif item_type == "equation":
                self._add_equation(doc, content)
                i += 1
            elif item_type == "newline":
                i = self._add_newline(doc, parsed, i)
            elif item_type in ("title", "section", "subsection", "subsubsection"):
                self._add_heading(doc, item_type, content)
                i += 1
            elif item_type == "table":
                self._add_table_to_doc(doc, content)
                i += 1
            elif item_type == "figure":
                self._add_figure_to_doc(doc, content)
                i += 1
            elif item_type == "itemize":
                self._add_itemize_to_doc(doc, content)
                i += 1
            else:  # enumerate
                self._add_enumerate_to_doc(doc, content)
                i += 1

    @staticmethod
    def _add_newline(doc: DocumentObject, parsed: list[dict[str, str | int]], start_index: int) -> int:
        """Add newline(s) to the document by adding empty paragraphs.

        Args:
            doc: The Word Document object.
            parsed: List of parsed content items.
            start_index: Starting index in parsed list.

        Returns
        -------
            Updated index after processing newlines.
        """
        # Count consecutive newlines and add empty paragraphs
        newline_count = 1
        j = start_index + 1
        while j < len(parsed) and str(parsed[j]["type"]) == "newline":
            newline_count += 1
            j += 1

        # Add empty paragraphs for each newline after the first
        # (the first newline just ends the current paragraph)
        for _ in range(newline_count - 1):
            empty_para = doc.add_paragraph()
            empty_para.style = "No Spacing"
            empty_para.paragraph_format.space_before = Pt(2)
            empty_para.paragraph_format.space_after = Pt(2)
        return j

    def _add_heading(self, doc: DocumentObject, heading_type: str, content: str) -> None:
        r"""Add a heading to the document.

        Args:
            doc: The Word Document object.
            heading_type: Type of heading ('title', 'section', 'subsection', or 'subsubsection').
            content: The LaTeX line containing the heading (e.g., '\\title{My Title}').
        """
        # Extract text from within braces (e.g., \title{text} -> text)
        brace_match = re.search(r"\\(?:title|section|subsection|subsubsection)\{(.*?)\}", content)
        if not brace_match:
            return  # pragma: no cover
        extracted_content = brace_match.group(1).strip()

        # Skip adding if \title{} is empty
        if heading_type == "title" and not extracted_content:
            return

        # Add numbering to sections
        if heading_type == "section":
            self.section_counter += 1
            self.subsection_counter = 0
            self.subsubsection_counter = 0
            numbered_content = f"{self.section_counter}. {extracted_content}"
        elif heading_type == "subsection":
            self.subsection_counter += 1
            self.subsubsection_counter = 0
            numbered_content = f"{self.section_counter}.{self.subsection_counter}. {extracted_content}"
        elif heading_type == "subsubsection":
            self.subsubsection_counter += 1
            numbered_content = f"{self.section_counter}.{self.subsection_counter}.{self.subsubsection_counter}. {extracted_content}"
        else:
            # Title doesn't get numbered
            numbered_content = extracted_content

        para = doc.add_paragraph(numbered_content)

        # Define style configurations for each heading type
        style_config: dict[str, dict[str, int | WD_PARAGRAPH_ALIGNMENT]] = {
            "title": {"size": 18, "space_before": 0, "space_after": 20, "alignment": WD_PARAGRAPH_ALIGNMENT.CENTER},
            "section": {"size": 14, "space_before": 8, "space_after": 4, "alignment": WD_PARAGRAPH_ALIGNMENT.LEFT},
            "subsection": {"size": 12, "space_before": 4, "space_after": 4, "alignment": WD_PARAGRAPH_ALIGNMENT.LEFT},
            "subsubsection": {"size": 12, "space_before": 4, "space_after": 0, "alignment": WD_PARAGRAPH_ALIGNMENT.LEFT},
        }

        config = style_config[heading_type]
        para.style = "No Spacing"
        run = para.runs[0]
        run.font.name = "Arial"
        run.font.size = Pt(int(config["size"]))
        run.font.bold = True
        run.font.color.rgb = RGBColor(0x00, 0x28, 0x55)
        para.paragraph_format.space_before = Pt(int(config["space_before"]))
        para.paragraph_format.space_after = Pt(int(config["space_after"]))
        para.alignment = cast(WD_PARAGRAPH_ALIGNMENT, config["alignment"])

    def _add_grouped_text(self, doc: DocumentObject, parsed: list[dict[str, str | int]], start_index: int) -> int:
        """Group consecutive text items into a single paragraph.

        Args:
            doc: The Word Document object.
            parsed: List of parsed content items.
            start_index: Starting index in parsed list.

        Returns
        -------
            Updated index after processing text items.
        """
        para = doc.add_paragraph()
        para.style = "No Spacing"
        para.paragraph_format.space_before = Pt(2)
        para.paragraph_format.space_after = Pt(2)

        i = start_index
        while i < len(parsed) and str(parsed[i]["type"]) == "text":
            text_item = parsed[i]
            content = str(text_item["content"]).strip()
            bold = bool(text_item.get("bold", False))
            italic = bool(text_item.get("italic", False))
            self._add_text_to_paragraph(para, content, bold, italic)
            i += 1

        return i

    def _add_text_to_paragraph(self, para: Paragraph, content: str, bold: bool = False, italic: bool = False) -> None:
        """Add text to an existing paragraph, applying bold/italic as needed.

        Args:
            para: The paragraph to add text to.
            content: The text content that may contain inline math ($...$).
            bold: Whether to apply bold formatting (default: False).
            italic: Whether to apply italic formatting (default: False).
        """
        segments = self._parse_text_blocks(content)
        if not segments:
            # If no text blocks found, parse inline equations and tags
            self._parse_and_add_inline_content(para, content, bold, italic)
        else:
            for segment in segments:
                segment_content = str(segment["content"])
                segment_bold = bool(segment.get("bold", False))
                segment_italic = bool(segment.get("italic", False))
                self._parse_and_add_inline_content(para, segment_content, segment_bold, segment_italic)

    def _parse_and_add_inline_content(self, para: Paragraph, content: str, bold: bool = False, italic: bool = False) -> None:
        """Parse content with inline equations and tags, adding them to the paragraph.

        Args:
            para: The paragraph to add content to.
            content: The text content that may contain inline math ($...$) and tags.
            bold: Whether to apply bold formatting (default: False).
            italic: Whether to apply italic formatting (default: False).
        """
        # Pattern to match inline equations ($...$) and capture surrounding text
        inline_eq_pattern = r"\$([^$]+)\$"

        parts = re.split(inline_eq_pattern, content)

        for i, part in enumerate(parts):
            if not part:
                continue

            # Even indices are regular text, odd indices are equation content
            if i % 2 == 0:
                # Regular text (possibly with tags)
                # Strip leading space if this text follows an equation
                text_to_add = part.lstrip() if i > 0 else part
                if text_to_add.strip():
                    run = para.add_run(text_to_add)
                    run.bold = bold
                    run.italic = italic
            else:
                # Equation content
                # Only add leading space if there was text before this equation
                if i > 0 and parts[i - 1].strip():
                    para.add_run(" ")
                para._p.append(self._formula(part.replace(r"\%", "%")))  # noqa: SLF001
                # Only add trailing space if there is text after this equation
                if i < len(parts) - 1 and parts[i + 1].strip():
                    para.add_run(" ")

    def _add_equation(self, doc: DocumentObject, content: str) -> None:
        r"""Add an equation to the document.

        If the equation is longer than 150 characters, split it on every '=' (except the first),
        or on every '\\to' (except the first), whichever comes first.

        Args:
            doc: The Word Document object.
            content: The LaTeX equation content.
        """
        # Extract tag if present
        tag_match = re.search(r"\\tag\{([^}]+)\}", content)
        tag = tag_match.group(1) if tag_match else None

        # Remove tag and \notag from equation content
        equation_content = re.sub(r"\s*\\tag\{[^}]+\}", "", content)
        equation_content = re.sub(r"\\notag", "", equation_content)

        p = doc.add_paragraph()
        p.style = "No Spacing"
        p.paragraph_format.space_before = Pt(6)
        p.paragraph_format.space_after = Pt(6)
        p.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER
        p._p.append(self._formula(equation_content))  # noqa: SLF001
        if tag:
            p.add_run(f" ({tag})")

    def _add_table_to_doc(self, doc: DocumentObject, table_latex: str) -> None:
        """Parse LaTeX table and add it to the Word document.

        Args:
            doc: The Word Document object.
            table_latex: LaTeX string containing a table environment.
        """
        rows_data = self._parse_table_content(table_latex)

        # Create Word table
        num_rows = len(rows_data)
        num_cols = max(len(row) for row in rows_data) if rows_data else 0

        table = doc.add_table(rows=num_rows, cols=num_cols)

        # Fill table cells (supporting inline math)
        for i, row_data in enumerate(rows_data):
            for j, cell_content in enumerate(row_data):
                if j < num_cols:
                    cell = table.rows[i].cells[j]
                    # Clear default paragraph and add content with inline math support
                    cell.text = ""
                    para = cell.paragraphs[0]
                    para.style = "No Spacing"
                    para.paragraph_format.space_before = Pt(2)
                    para.paragraph_format.space_after = Pt(2)
                    # Make top row bold
                    is_bold = i == 0
                    self._add_text_to_paragraph(para, cell_content, bold=is_bold)

        # Apply table styling: lines above header, below header, and below table
        self._apply_table_styling(table)

    @staticmethod
    def _apply_table_styling(table: Any) -> None:  # noqa: ANN401
        """Apply styling to a table: line above header, below header, and below entire table.

        Args:
            table: The Word table object to style.
        """

        def set_cell_border(cell: Any, **kwargs: Any) -> None:  # noqa: ANN401
            """Set border for a cell.

            Args:
                cell: The cell to set border for.
                **kwargs: Border specifications (top, bottom, left, right, etc.)
            """
            tc = cell._element  # noqa: SLF001
            tc_pr = tc.get_or_add_tcPr()

            # Create borders element
            tc_borders = OxmlElement("w:tcBorders")

            for edge in ("top", "left", "bottom", "right"):
                if edge in kwargs:
                    border = OxmlElement(f"w:{edge}")
                    border.set(qn("w:val"), kwargs[edge]["val"])
                    border.set(qn("w:sz"), kwargs[edge]["sz"])
                    border.set(qn("w:space"), kwargs[edge]["space"])
                    border.set(qn("w:color"), kwargs[edge]["color"])
                    tc_borders.append(border)

            tc_pr.append(tc_borders)

        # Define border style for lines
        line_border = {"val": "single", "sz": "12", "space": "0", "color": "000000"}

        # Apply border above header (first row)
        if len(table.rows) > 0:
            for cell in table.rows[0].cells:
                set_cell_border(cell, top=line_border)

        # Apply border below header (first row)
        if len(table.rows) > 0:
            for cell in table.rows[0].cells:
                set_cell_border(cell, bottom=line_border)

        # Apply border below entire table (last row)
        if len(table.rows) > 0:
            for cell in table.rows[-1].cells:
                set_cell_border(cell, bottom=line_border)

    def _add_figure_to_doc(self, doc: DocumentObject, figure_latex: str) -> None:
        """Parse LaTeX figure and add it to the Word document.

        Args:
            doc: The Word Document object.
            figure_latex: LaTeX string containing a figure environment.
        """
        figure_info = self._parse_figure_content(figure_latex)
        if not figure_info:
            return  # pragma: no cover

        # Add the image to the document
        para = doc.add_paragraph()
        run = para.add_run()
        para.style = "No Spacing"
        para.paragraph_format.space_before = Pt(6)
        para.paragraph_format.space_after = Pt(6)
        para.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER

        # Get width from figure info (default to 3 inches if not specified)
        width = float(figure_info.get("width_inches", 3.0))

        # Add picture to the run
        run.add_picture(str(figure_info["image_path"]), width=Inches(width))

        # Add caption if present
        caption = figure_info.get("caption")
        if caption:
            caption_para = doc.add_paragraph()
            caption_para.style = "No Spacing"
            caption_para.paragraph_format.space_before = Pt(6)
            caption_para.paragraph_format.space_after = Pt(6)
            caption_para.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER
            run = caption_para.add_run(str(caption))
            run.italic = True

    def _add_itemize_to_doc(self, doc: DocumentObject, itemize_latex: str) -> None:
        """Parse LaTeX itemize list and add it to the Word document.

        Args:
            doc: The Word Document object.
            itemize_latex: LaTeX string containing an itemize environment.
        """
        items = self._find_items_with_nested_level_and_index(itemize_latex)

        for item in items:
            para = doc.add_paragraph()
            para.style = "No Spacing"
            para.paragraph_format.space_before = Pt(2)
            para.paragraph_format.space_after = Pt(2)
            para.paragraph_format.tab_stops.add_tab_stop(Inches(0.15))
            para.paragraph_format.tab_stops.add_tab_stop(Inches(0.30))
            para.paragraph_format.tab_stops.add_tab_stop(Inches(0.45))
            # Clear default text and add content with inline math support
            para.text = ""
            # Add the bullet prefix
            para.add_run("\t" * int(item["level"]) + "•    ")
            self._add_text_to_paragraph(para, str(item["content"]))

    def _add_enumerate_to_doc(self, doc: DocumentObject, enumerate_latex: str) -> None:
        """Parse LaTeX enumerate list and add it to the Word document.

        Args:
            doc: The Word Document object.
            enumerate_latex: LaTeX string containing an enumerate environment.
        """
        items = self._find_items_with_nested_level_and_index(enumerate_latex)

        for item in items:
            para = doc.add_paragraph()
            para.style = "No Spacing"
            para.paragraph_format.space_before = Pt(2)
            para.paragraph_format.space_after = Pt(2)
            para.paragraph_format.tab_stops.add_tab_stop(Inches(0.15))
            para.paragraph_format.tab_stops.add_tab_stop(Inches(0.30))
            para.paragraph_format.tab_stops.add_tab_stop(Inches(0.45))
            # Clear default text and add content with inline math support
            para.text = ""
            # Add the number prefix
            para.add_run("\t" * int(item["level"]) + f"{item['index']}  ")
            self._add_text_to_paragraph(para, str(item["content"]))

    @staticmethod
    def _formula(latex_string: str) -> BaseOxmlElement:
        """Convert a LaTeX equation string (already in math mode) to an OMML XML element for Word.

        Args:
            latex_string: The LaTeX string representing the equation.

        Returns
        -------
            BaseOxmlElement containing the OMML representation of the equation.
        """
        mathml_output = latex2mathml.converter.convert(latex_string)
        omml_output = mathml2omml.convert(mathml_output)
        xml_output = f'<m:oMathPara xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">{omml_output}</m:oMathPara>'
        return parse_xml(xml_output)[0]

    @staticmethod
    def _parse_text_blocks(text: str) -> list[dict[str, str | bool]]:
        r"""Parse text blocks (\textbf, \textit, \text).

        Args:
            text: LaTeX string without equation environments or newlines.

        Returns
        -------
            List of dictionaries representing text items.
        """
        result: list[dict[str, str | bool]] = []
        # Pattern matches \textbf{...}, \textit{...}, \text{...} or \txt{...} with nested braces
        pattern = (
            r"\\textbf\{((?:[^{}]|\{[^{}]*\})*)\}"
            r"|\\textit\{((?:[^{}]|\{[^{}]*\})*)\}"
            r"|\\text\{((?:[^{}]|\{[^{}]*\})*)\}"
            r"|\\txt\{((?:[^{}]|\{[^{}]*\})*)\}"
        )
        for match in re.finditer(pattern, text):
            bold_content = match.group(1)
            italic_content = match.group(2)
            text_content = match.group(3)
            txt_content = match.group(4)
            if bold_content is not None:
                content = bold_content
                bold, italic = True, r"\textit{" in content
                if italic:
                    nested = re.search(r"\\textit\{((?:[^{}]|\{[^{}]*\})*)\}", content)
                    if nested:
                        content = nested.group(1)
                result.append({"type": "text", "content": content, "bold": bold, "italic": italic})
            elif italic_content is not None:
                content = italic_content
                bold, italic = False, True
                result.append({"type": "text", "content": content, "bold": bold, "italic": italic})
            elif text_content is not None or txt_content is not None:
                content = text_content if text_content is not None else txt_content
                result.append({"type": "text", "content": content, "bold": False, "italic": False})
        return result

    @staticmethod
    def _parse_table_content(table_latex: str) -> list[list[str]]:
        """Parse LaTeX table content into rows and columns.

        Args:
            table_latex: LaTeX string containing a table environment.

        Returns
        -------
            List of rows, where each row is a list of cell contents.
        """
        # Extract tabular content
        tabular_match = re.search(r"\\begin\{tabular\}\{.*?\}(.*?)\\end\{tabular\}", table_latex, re.DOTALL)
        if not tabular_match:
            return []  # pragma: no cover
        tabular_content = tabular_match.group(1)

        # Remove LaTeX table commands
        tabular_content = re.sub(r"\\toprule|\\midrule|\\bottomrule|\\hline|\\centering", "", tabular_content)

        # Split by rows (\\)
        rows = re.split(r"\\\\", tabular_content)

        parsed_rows = []
        for row in rows:
            row_stripped = row.strip()
            if not row_stripped:
                continue
            # Split by columns (&)
            cells = row_stripped.split("&")
            parsed_cells = []
            for cell in cells:
                cell_stripped = cell.strip()
                # Extract text from \text{...} blocks while preserving inline math
                # Handle nested braces: match \text{...} with up to one level of nested braces
                cell_content = re.sub(r"\\text\{((?:[^{}]|\{[^{}]*\})*)\}", r"\1", cell_stripped)
                parsed_cells.append(cell_content)
            if parsed_cells:
                parsed_rows.append(parsed_cells)

        return parsed_rows

    @staticmethod
    def _parse_figure_content(figure_latex: str) -> dict[str, str | float] | None:
        """Parse LaTeX figure content to extract image path and properties.

        Args:
            figure_latex: LaTeX string containing a figure environment.

        Returns
        -------
            Dictionary with image_path and optional width_inches, or None if no valid image found.
        """
        # Extract includegraphics command
        # Pattern: \includegraphics[options]{filename}
        includegraphics_pattern = r"\\includegraphics(?:\[(.*?)\])?\{(.*?)\}"
        match = re.search(includegraphics_pattern, figure_latex)
        if not match:
            return None  # pragma: no cover
        options_str = match.group(1) if match.group(1) else ""
        image_path = match.group(2).strip()

        result: dict[str, str | float] = {"image_path": image_path}

        # Parse options for width
        if options_str:
            # Look for width specification (e.g., width=0.5\textwidth)
            width_match = re.search(r"width\s*=\s*([0-9.]+)\\textwidth", options_str)
            if width_match:
                result["width_inches"] = float(width_match.group(1)) * 6  # textwidth is 6 inches

        # Extract caption if present
        caption_match = re.search(r"\\caption\{((?:[^{}]|\{[^{}]*\})*)\}", figure_latex)
        if caption_match:
            result["caption"] = caption_match.group(1).strip()

        return result

    @staticmethod
    def _find_items_with_nested_level_and_index(latex_content: str) -> list[dict[str, str | int]]:
        """Determine the nesting level with index of an item at a given position in LaTeX content.

        Args:
            latex_content: The LaTeX string containing nested lists.

        Returns
        -------
            A list of dictionaries with 'content', 'level', and 'index' keys.
        """
        items_with_levels: list[dict[str, str | int]] = []

        # Patterns to identify begin/end of lists and items
        # Find all begin and end positions
        begin_matches = [m.start() for m in re.finditer(r"\\begin\{(itemize|enumerate)\}", latex_content)]
        end_matches = [m.start() for m in re.finditer(r"\\end\{(?:itemize|enumerate)\}", latex_content)]

        # Find all \item positions and their content
        item_pattern = r"\\item\s*"
        item_matches = list(re.finditer(item_pattern, latex_content))

        # For each item, find its content (up to next \item, \begin, \end, or end of string)
        items_content: list[dict[str, str | int]] = []
        for i, item_match in enumerate(item_matches):
            start = item_match.end()

            # Find the next boundary: \item, \begin, \end, or end of string
            next_item = item_matches[i + 1].start() if i + 1 < len(item_matches) else len(latex_content)
            next_begin = min([pos for pos in begin_matches if pos > start] + [len(latex_content)])
            next_end = min([pos for pos in end_matches if pos > start] + [len(latex_content)])

            # Take the minimum of all boundaries
            end = min(next_item, next_begin, next_end)

            item_content = latex_content[start:end].strip()
            items_content.append({"content": item_content, "position": item_match.start()})

        # Now determine nesting level for each item based on begin/end counts
        for item_info in items_content:
            pos = int(item_info["position"])
            # Count how many \begin before this position minus how many \end before this position
            begins_before = sum(1 for b in begin_matches if b < pos)
            ends_before = sum(1 for e in end_matches if e < pos)
            level = begins_before - ends_before - 1
            items_with_levels.append({"content": str(item_info["content"]), "level": level})

        # Find maximum nesting level and create counters
        max_level = max((int(item["level"]) for item in items_with_levels), default=0)
        level_counters = np.zeros(max_level + 1, dtype=int)

        # Assign indexes based on levels
        for item in items_with_levels:
            level = int(item["level"])
            level_counters[level] += 1
            # Reset counters for deeper levels
            level_counters[level + 1 :] = 0
            item["index"] = _ReportToWordConverter._get_number_format(int(level_counters[level]), level)

        # align length of indexes
        max_index_length = max((len(str(item["index"])) for item in items_with_levels), default=0)
        for item in items_with_levels:
            item["index"] = str(item["index"]).ljust(max_index_length)

        return items_with_levels

    @staticmethod
    def _get_number_format(number: int, level: int) -> str:
        """Set the numbering format for a paragraph based on its nesting level.

        This method supports up to 4 nesting levels (levels 0-3). For non-top levels
        (level > 0), when number > 26, it gracefully falls back to a numeric format
        (e.g., "27.") rather than failing. Level 0 (top level) uses numeric formatting
        without the 26-item limitation.

        Formats used for each level:
        - level 0: "1." (numeric)
        - level 1: "(a)" (lowercase letters in parentheses)
        - level 2: "i." (lowercase Roman numerals)
        - level 3: "A." (uppercase letters)

        Args:
            number: The number of the item in the list.
            level: The nesting level (0-based).

        Returns
        -------
            Formatted string for the list item number (e.g., "1.", "(a)", "i.", "A.").
        """
        # Define numbering formats for levels
        if level > 0 and number > 26:
            return f"{number}."

        formats = {
            0: f"{number}.",  # Level 1: 1., 2., 3., ...
            1: f"({chr(96 + number)})",  # Level 2: (a), (b), (c), ...
            2: [
                "i",
                "ii",
                "iii",
                "iv",
                "v",
                "vi",
                "vii",
                "viii",
                "ix",
                "x",
                "xi",
                "xii",
                "xiii",
                "xiv",
                "xv",
                "xvi",
                "xvii",
                "xviii",
                "xix",
                "xx",
                "xxi",
                "xxii",
                "xxiii",
                "xxiv",
                "xxv",
                "xxvi",
            ][number - 1]
            + ".",  # Level 3: i., ii., iii., ...
            3: f"{chr(64 + number)}.",  # Level 4: A., B., C., ...
        }
        return formats.get(level, f"{number}.")
