"""
Markdown processing utilities for converting between markdown and structured data.
This module provides functions and classes to convert markdown text into nested
dictionaries and back, enabling programmatic manipulation of markdown content.
"""

from collections import OrderedDict
from collections.abc import Callable
from functools import reduce
import json
import operator
from typing import Any

from .vendor import CommonMark


# Type stub for Block to handle missing attributes
class BlockType:
    """Type stub for CommonMark Block with common attributes."""

    t: str
    level: int
    children: list[Any]
    strings: list[Any]
    string_content: str
    inline_content: list[Any]
    label: list[Any]
    c: list[Any]
    destination: str
    title: str


def dictify(markdown_str: str) -> OrderedDict[str, Any]:
    """
    Convert a markdown string into a nested Python dictionary.
    This function parses markdown text and converts it into a structured
    dictionary representation that can be programmatically manipulated.
    Args:
        markdown_str: The markdown text to convert
    Returns:
        A nested dictionary representation of the markdown content
    Example:
        >>> markdown_text = "# Title\\n\\nSome content"
        >>> result = dictify(markdown_text)
        >>> print(result)
        OrderedDict([('Title', 'Some content')])
    """
    ast = CommonMark.DocParser().parse(markdown_str)
    nested = CMarkASTNester().nest(ast)
    return Renderer().stringify_dict(nested)


def jsonify(markdown_str: str) -> str:
    """
    Convert a markdown string into a JSON string.
    This is a convenience function that first converts markdown to a dictionary
    and then serializes it to JSON.
    Args:
        markdown_str: The markdown text to convert
    Returns:
        A JSON string representation of the markdown content
    Example:
        >>> markdown_text = "# Title\\n\\nSome content"
        >>> json_str = jsonify(markdown_text)
        >>> print(json_str)
        '{"Title": "Some content"}'
    """
    d = dictify(markdown_str)
    return json.dumps(d)


def markdownify(data: dict[str, Any] | str) -> str:
    """
    Convert a dictionary or JSON string back into markdown.
    This function can convert either a dictionary directly or a JSON string
    that was previously created from markdown back into markdown format.
    Args:
        data: Dictionary or JSON string to convert
    Returns:
        Markdown formatted string
    Raises:
        ValueError: If the input is not valid JSON (when string is provided)
    Example:
        >>> data = {"Title": "Some content"}
        >>> markdown_text = markdownify(data)
        >>> print(markdown_text)
        '# Title\\n\\nSome content'
    """
    if isinstance(data, str):
        if not data:
            return ""
        # Check if it's a JSON string
        if data.strip().startswith("{") or data.strip().startswith("["):
            try:
                data = json.loads(data)
            except json.JSONDecodeError as e:
                raise ValueError(f"Invalid JSON string: {e}") from e
        else:
            # It's a plain string, return as-is
            return data
    if not isinstance(data, dict):
        return str(data)
    return _dict_to_markdown(data)


def _dict_to_markdown(data: Any, level: int = 1) -> str:
    """
    Recursively convert dictionary back to markdown.
    Args:
        data: Dictionary to convert
        level: Current heading level (default: 1)
    Returns:
        Markdown formatted string
    """
    if not isinstance(data, dict):
        return str(data)
    result: list[str] = []
    for key, value in data.items():
        # Skip root key as it's just a container
        if key == "root":
            return _dict_to_markdown(value, level)
        # Handle headers
        if isinstance(key, str):
            result.append(f"{'#' * level} {key.strip()}")
        # Handle nested content
        if isinstance(value, dict):
            result.append(_dict_to_markdown(value, level + 1))
        elif isinstance(value, list):
            # Handle list items
            for item in value:
                if isinstance(item, dict):
                    result.append(_dict_to_markdown(item, level + 1))
                else:
                    result.append(str(item))
        else:
            result.append(str(value))
    return "\n\n".join(filter(None, result))


class CMarkASTNester:
    """
    Nests DOM (Document Object Model) into a Python dictionary.
    This class processes the abstract syntax tree (AST) generated by CommonMark
    and converts it into a nested dictionary structure that represents the
    hierarchical nature of markdown content.
    """

    def nest(self, ast: Any) -> Any:
        """
        Outermost nest call to process the AST.
        Handles documents with ## as the top ATX header by finding the minimum
        heading level and using that as the base level for nesting.
        Args:
            ast: The CommonMark AST block to process
        Returns:
            Nested dictionary or list representation of the AST
        """
        # Handle documents with ## as the top ATX header.
        parts = [
            block.level if hasattr(block, "level") else 100000 for block in ast.children
        ]
        if parts:
            minimum = min(parts)
            return self._dictify_blocks(ast.children, minimum)
        return []

    def _dictify_blocks(self, blocks: list[Any], heading_level: int) -> Any:
        """
        Recursive nest call to process blocks at a specific heading level.
        Args:
            blocks: List of blocks to process
            heading_level: The heading level to match for nesting
        Returns:
            Nested dictionary or list representation of the blocks
        """

        def matches_heading(block: Any) -> bool:
            """Filter function to match headers at the specified level."""
            return (
                hasattr(block, "t")
                and hasattr(block, "level")
                and block.t == "ATXHeader"
                and block.level == heading_level
            )

        if not any(matches_heading(b) for b in blocks):
            self._ensure_list_singleton(blocks)
            return blocks
        splitted = dictify_list_by(blocks, matches_heading)
        for heading, nests in splitted.items():
            splitted[heading] = self._dictify_blocks(nests, heading_level + 1)
        return splitted

    def _ensure_list_singleton(self, blocks: list[Any]) -> None:
        """
        Make sure lists don't mix content.
        This method is currently a placeholder for future list content
        validation logic.
        Args:
            blocks: List of blocks to validate
        """
        # Placeholder for list content validation
        pass


class ContentError(ValueError):
    """Exception raised for content-related errors in markdown processing."""

    def __init__(self, message: str, details: dict[str, Any] | None = None) -> None:
        """
        Initialize ContentError with message and optional details.
        Args:
            message: Error message
            details: Optional additional details about the error
        """
        super().__init__(message)
        self.details = details


def dictify_list_by(
    list_of_blocks: list[Any], filter_function: Callable[[Any], bool]
) -> dict[Any, list[Any]]:
    """
    Turn list of tokens into dictionary of lists of tokens.
    This function groups blocks based on a filter function, typically used
    to group content under headings.
    Args:
        list_of_blocks: List of blocks to process
        filter_function: Function to determine grouping criteria
    Returns:
        Dictionary mapping filtered items to their associated content
    Raises:
        ValueError: If filter_function is None
    """
    if filter_function is None:
        raise ValueError("Key function cannot be None")
    result: OrderedDict[Any, list[Any]] = OrderedDict()
    cur: Any | None = None
    children: list[Any] = []
    for item in list_of_blocks:
        if filter_function(item):
            if cur is not None:
                # Pop cur, children into result
                result[cur] = children
            cur = item
            children = []
            continue
        children.append(item)
    if cur is not None:
        result[cur] = children
    return result


class Renderer:
    """
    Processes DOM (Document Object Model) into string representations.
    This class handles the conversion of CommonMark AST blocks into various
    string formats, supporting different markdown elements like headers,
    lists, code blocks, etc.
    """

    def stringify_dict(self, dictionary: Any) -> OrderedDict[str, Any]:
        """
        Create dictionary of keys and values as strings.
        Args:
            dictionary: Dictionary or list to convert to string representation
        Returns:
            OrderedDict with string keys and processed values
        """
        if isinstance(dictionary, dict):
            out = OrderedDict()
            for k, v in dictionary.items():
                # Handle string keys directly
                key = k if isinstance(k, str) else self._render_block(k)
                out[key] = self._valuify(v)
        else:
            out = OrderedDict([("root", [self._render_block(v) for v in dictionary])])
        return out

    def _valuify(self, cm_vals: Any) -> Any:
        """
        Render values of dictionary as scalars or lists.
        Args:
            cm_vals: Values to render
        Returns:
            Rendered values as strings or lists
        """
        if hasattr(cm_vals, "items"):
            return self.stringify_dict(cm_vals)
        # Handle non-list values (strings, numbers, etc.)
        if not isinstance(cm_vals, list):
            return str(cm_vals)
        if len(cm_vals) == 0:
            return ""
        first = cm_vals[0]
        if hasattr(first, "t") and first.t == "List":
            return self._render_List(first)
        # HACK: This is just str'ing the unexpected lists
        return "\n\n".join([str(self._render_block(v)) for v in cm_vals])

    def _render_block(self, block: Any) -> Any:
        """
        Render any block based on its type.
        Args:
            block: The block to render
        Returns:
            String or list representation of the block
        """
        if not hasattr(block, "t"):
            return str(block)
        method_name = f"_render_{block.t}"
        method = self._render_generic_block
        if hasattr(self, method_name):
            method = getattr(self, method_name)
        return method(block)

    def _render_generic_block(self, block: Any) -> Any:
        """
        Render any block using generic logic.
        Args:
            block: The block to render
        Returns:
            String or list representation of the block
        """
        if (
            hasattr(block, "strings")
            and hasattr(block.strings, "__len__")
            and len(block.strings) > 0
        ):
            return "\n".join(
                item.decode("utf8") if isinstance(item, bytes) else item
                for item in block.strings
            )
        if len(block.children) > 0:
            return [self._render_block(b) for b in block.children]
        # Is this an error state?
        return []

    def _render_List(self, block: Any) -> Any:
        """
        Render list blocks.
        Args:
            block: The list block to render
        Returns:
            List of rendered list items
        """
        # We need to de-nest this one level -- we'll use the trick that
        # lists can be added to do this.
        list_items = [self._render_block(li) for li in block.children]
        if not list_items:
            return []
        return reduce(operator.add, list_items)

    def _render_FencedCode(self, block: Any) -> str:
        """
        Render fenced code blocks.
        Args:
            block: The fenced code block to render
        Returns:
            String representation of the code block with markdown formatting
        """
        content = getattr(block, "string_content", "")
        return "```\n" + str(content) + "```"
