"""
This module is responsible for parsing Markdown text into a structured list
of dictionaries, often referred to as an Abstract Syntax Tree (AST) or
"block dicts". These block dicts represent the various elements of the
Markdown document (e.g., headings, paragraphs, lists, code blocks) in a
format that can be easily consumed by renderers.

It leverages the `markdown-it-py` library for the initial parsing into
a stream of tokens, and then further processes these tokens to create
a simplified, dictionary-based representation suitable for Slate's rendering
pipeline.
"""


import re
from typing import Any

from markdown_it import MarkdownIt
from markdown_it.token import Token
from mdit_py_plugins.front_matter import front_matter_plugin

# Define a list of recognized callout types.
# Callouts are special blocks that highlight information, warnings, etc.
CALLOUTS = ("NOTE", "WARNING", "DANGER", "SUCCESS", "TIP")


def parse_markdown_to_dicts(mdtext: str) -> list[dict[str, Any]]:
    """Parses a Markdown string into a list of block dictionaries.

    This function takes raw Markdown text, processes it using the markdown-it-py
    library, and then transforms the resulting tokens into a more semantic
    list of dictionaries. Each dictionary represents a distinct block-level
    element of the Markdown document, such as a heading, paragraph, list,
    or code block.

    Args:
        mdtext: The input Markdown text as a string.

    Returns:
        A list of dictionaries, where each dictionary represents a parsed
        block from the Markdown document.
    """
    # Initialize the MarkdownIt parser with commonmark rules.
    # 'breaks': True ensures that newline characters are treated as hard breaks.
    # 'html': True allows raw HTML to be included (though not directly processed
    #         by this parser, it influences how markdown-it handles content).
    md = (
        MarkdownIt('commonmark', {'breaks':True,'html':True})
        # Enable front matter plugin to handle YAML front matter if present.
        .use(front_matter_plugin)
        # Enable table parsing.
        .enable('table')
    )
    # The .use() and .enable() calls can be chained directly to the MarkdownIt
    # constructor, making the subsequent separate calls redundant.
    # md.use(front_matter_plugin) # Redundant, already chained above.
    # md.enable("table")          # Redundant, already chained above.
    
    # Parse the Markdown text into a stream of tokens.
    # These tokens are the intermediate representation generated by markdown-it-py.
    tokens = md.parse(mdtext)


    # This list will store the final block dictionaries that represent the Markdown document.
    result: list[dict[str, Any]] = []
    
    # Initialize an index to iterate through the tokens list.
    i = 0
    
    
    
    
    
        # Helper function to parse lists (bullet or ordered) recursively.
    
        # This function is defined here to allow it to access `result` and `tokens` from the outer scope,
    
        # and to simplify the main parsing loop by encapsulating list-specific logic.

    def parse_list_at(tokens: list[Token], idx: int) -> tuple[dict[str, Any], int]:
        """Parses a bullet or ordered list block starting at the given token index.

        This is a recursive helper function that processes a list and its nested
        sub-lists. It identifies list items, their content (which can be plain
        text or a paragraph), and any lists nested within them.

        Args:
            tokens: The list of tokens generated by markdown-it-py.
            idx: The starting index in the `tokens` list where the list begins
                 (i.e., at a 'bullet_list_open' or 'ordered_list_open' token).

        Returns:
            A tuple containing:
            - list_block_dict: A dictionary representing the parsed list.
                               It will have a key 'ul' for unordered lists or 'ol'
                               for ordered lists, with its value being a list of
                               parsed list items.
            - new_index: The index in the `tokens` list immediately after the
                         closing token of the parsed list.
        """
        # Get the opening token for the list (either bullet or ordered).
        start_token = tokens[idx]
        is_ordered = start_token.type == "ordered_list_open"
        list_key = "ol" if is_ordered else "ul"
        
        # This list will hold the processed items of the current list.
        items: list[Any] = []
        
        # Start iterating from the token immediately after the list_open token.
        j = idx + 1
        
        # Determine the expected closing token type for the current list.
        close_type = "ordered_list_close" if is_ordered else "bullet_list_close"
        
        # Loop through tokens until the closing token of the current list is found.
        while j < len(tokens) and tokens[j].type != close_type:
            current_token = tokens[j]
            
            # If the current token marks the opening of a list item.
            if current_token.type == "list_item_open":
                # Initialize variables to store content and nested lists for the current item.
                item_text = None
                nested_list_data = None
                
                # Start parsing the contents of the list item.
                k = j + 1
                while k < len(tokens) and tokens[k].type != "list_item_close":
                    item_content_token = tokens[k]
                    
                    # If a paragraph opens within the list item, its content is the item's text.
                    if item_content_token.type == "paragraph_open":
                        # Check if the next token is an 'inline' token, which contains the actual text.
                        if k + 1 < len(tokens) and tokens[k+1].type == "inline":
                            item_text = tokens[k+1].content
                            k += 2  # Skip both paragraph_open and inline tokens.
                            continue
                        k += 1  # Skip only paragraph_open if no inline follows immediately.
                    # If a nested list opens within the list item, recursively parse it.
                    elif item_content_token.type in ("bullet_list_open", "ordered_list_open"):
                        nested_list_data, new_k_after_nested_list = parse_list_at(tokens, k)
                        k = new_k_after_nested_list  # Update index after processing nested list.
                        continue
                    else:
                        k += 1 # Move to the next token within the list item.
                
                # After parsing the list item's content, determine its representation.
                item_representation: Any
                if nested_list_data and item_text:
                    # If both text and a nested list are present, combine them in a dictionary.
                    item_representation = {"p": item_text}
                    item_representation.update(nested_list_data)
                elif nested_list_data:
                    # If only a nested list is present, represent it directly (wrapped in dict for consistency).
                    item_representation = nested_list_data
                else:
                    # If only text (or no content) is present, use the text string, defaulting to empty.
                    item_representation = item_text or ""

                # Add the parsed item to the current list's items.
                items.append(item_representation)
                
                # Move the main index 'j' past the current list item and its closing token.
                j = k + 1
            else:
                j += 1  # Move to the next token if it's not a list item (e.g., whitespace).

        # Return the structured list data and the index after the list's closing token.
        return ({list_key: items}, j + 1)

    # The main loop iterates through the tokens generated by markdown-it-py.
    # It processes each token or sequence of tokens to identify Markdown blocks
    # and converts them into the desired dictionary format.
    while i < len(tokens):
        current_token = tokens[i]


        # --- Handle Headings (H1, H2, H3, etc.) ---
        # A heading consists of a 'heading_open' token, followed by an 'inline' token
        # containing the heading text, and then a 'heading_close' token.
        if current_token.type == "heading_open":
            # Extract the heading level (e.g., '1' from 'h1').
            heading_tag = f"h{current_token.tag[1]}"
            # Get the actual text content of the heading.
            heading_text = tokens[i+1].content if tokens[i+1].type == "inline" else ""
            # Add the heading block to the result list.
            result.append({heading_tag: heading_text})
            # Skip the 'inline' token and the 'heading_close' token.
            i += 3  # heading_open, inline, heading_close (should be 3 tokens total)
            continue


        # --- Handle Paragraphs and Callouts ---
        # A paragraph starts with 'paragraph_open', contains an 'inline' token,
        # and ends with 'paragraph_close'. Callouts are special paragraphs.
        if current_token.type == "paragraph_open":
            # Get the content of the paragraph.
            paragraph_content = tokens[i+1].content if tokens[i+1].type == "inline" else ""
            
            # Check if this paragraph is a special "callout" block.
            is_callout = False
            for callout_type in CALLOUTS:
                # Callouts are identified by patterns like "[!NOTE]" at the start of a paragraph.
                callout_prefix = f"[!{callout_type}]"
                if paragraph_content.strip().upper().startswith(callout_prefix):
                    # Extract the text after the callout prefix.
                    stripped_content = paragraph_content.strip()[len(callout_prefix):].strip()
                    # Add the callout block to the result list.
                    result.append({f"callout-{callout_type.lower()}": stripped_content})
                    is_callout = True
                    break
            
            # If it's a regular paragraph and not a callout, add it to the result.
            if not is_callout:
                result.append({"p": paragraph_content})
            
            # Skip the 'inline' token and the 'paragraph_close' token.
            i += 3 # paragraph_open, inline, paragraph_close (should be 3 tokens total)
            continue


        # --- Handle Blockquotes ---
        # A blockquote starts with 'blockquote_open', contains content (often 'inline'),
        # and ends with 'blockquote_close'.
        if current_token.type == "blockquote_open":
            blockquote_text = ""
            j = i + 1 # Start checking tokens after 'blockquote_open'.
            # Accumulate all inline content until 'blockquote_close'.
            while tokens[j].type != "blockquote_close":
                if tokens[j].type == "inline":
                    blockquote_text += tokens[j].content
                j += 1
            # Add the blockquote block to the result list.
            result.append({"blockquote": blockquote_text})
            # Move index 'i' past the entire blockquote.
            i = j + 1
            continue


        # --- Handle Images (which can appear inline) ---
        # markdown-it-py represents images as children of an 'inline' token.
        if current_token.type == "inline":
            # Check if this inline token has children, and if any child is an 'image'.
            for child_token in getattr(current_token, "children", []):
                if child_token.type == "image":
                    # Extract image source, alt text, and caption (title).
                    image_source = child_token.attrs.get("src", "")
                    image_alt_text = child_token.attrs.get("alt", "")
                    image_caption = child_token.attrs.get("title", "")
                    
                    # Add the image block to the result list.
                    result.append({"image": {
                        "src": image_source,
                        "alt": image_alt_text,
                        "caption": image_caption
                    }})
            # Move to the next token.
            i += 1
            continue


        # --- Handle Code Blocks (fenced code blocks) ---
        # Fenced code blocks are represented by a 'fence' token.
        if current_token.type == "fence":
            # Extract the code content and the language info (e.g., 'python').
            code_content = current_token.content
            code_language = current_token.info or "" # Default to empty string if no language specified.
            # Add the code block to the result list.
            result.append({"code": {
                "text": code_content,
                "lang": code_language
            }})
            # Move to the next token.
            i += 1
            continue


        # --- Handle Unordered Lists ---
        # An unordered list starts with 'bullet_list_open'.
        if current_token.type == "bullet_list_open":
            # Use the helper function to parse the entire list block.
            list_block_data, next_index = parse_list_at(tokens, i)
            # Add the parsed list to the result.
            result.append(list_block_data)
            # Update the main index 'i' to after the parsed list.
            i = next_index
            continue


        # --- Handle Ordered Lists ---
        # An ordered list starts with 'ordered_list_open'.
        if current_token.type == "ordered_list_open":
            # Use the helper function to parse the entire list block.
            list_block_data, next_index = parse_list_at(tokens, i)
            # Add the parsed list to the result.
            result.append(list_block_data)
            # Update the main index 'i' to after the parsed list.
            i = next_index
            continue


        # --- Handle Table Parsing ---
        # A table starts with 'table_open' and involves multiple sub-tokens.
        if current_token.type == "table_open":
            table_headers: list[str] = []
            table_rows: list[list[str]] = []
            
            # Find table headers (thead).
            j = i + 1 # Start after 'table_open'.
            while tokens[j].type != "thead_close":
                # Header cells are within 'th_open' and 'th_close'.
                if tokens[j].type == "th_open":
                    # The actual header text is in the 'inline' token after 'th_open'.
                    table_headers.append(tokens[j+1].content)
                j += 1 # Move to the next token in the header section.
            
            # Find table rows (tbody).
            k = j + 1 # Start after 'thead_close'.
            while tokens[k].type != "table_close":
                # Each table data cell starts with 'td_open'.
                if tokens[k].type == "td_open":
                    current_row_cells = []
                    k += 1 # Move past 'td_open'.
                    # Collect all inline content for the current row's cells until 'tr_close'.
                    while tokens[k].type != "tr_close":
                        if tokens[k].type == "inline":
                            current_row_cells.append(tokens[k].content)
                        k += 1 # Move to the next token in the row.
                    table_rows.append(current_row_cells)
                else:
                    k += 1 # Move to the next token in the table body.
            
            # Add the table block to the result list.
            result.append({"table": {"headers": table_headers, "rows": table_rows}})
            # Update the main index 'i' past the entire table block.
            i = k + 1
            continue

        # If the token is not recognized or already handled, just move to the next.
        i += 1

    return result


def generate_toc(blocks: list[dict[str, Any]]) -> str:
    """Generate table of contents HTML from heading blocks.
    
    Extracts all h1-h6 headings and creates a nested HTML list with slugified
    anchor links.
    
    Args:
        blocks: List of parsed Markdown blocks
        
    Returns:
        HTML string with table of contents, or empty string if no headings
    """
    toc_items = []
    
    # Extract heading information
    for block in blocks:
        for level_key in ["h1", "h2", "h3", "h4", "h5", "h6"]:
            if level_key in block:
                text = block[level_key]
                level = int(level_key[1])
                slug = slugify(text)
                toc_items.append({
                    "level": level,
                    "text": text,
                    "slug": slug
                })
                break  # Only one heading per block
    
    if not toc_items:
        return ""
    
    # Build HTML
    html_lines = ['<nav class="toc">', '  <ul>']
    
    for item in toc_items:
        level_class = f' class="toc-h{item["level"]}"' if item["level"] > 1 else ""
        html_lines.append(
            f'    <li{level_class}><a href="#{item["slug"]}">{item["text"]}</a></li>'
        )
    
    html_lines.extend(['  </ul>', '</nav>'])
    
    return '\n'.join(html_lines)


def slugify(text: str) -> str:
    """Convert text to URL-safe slug.
    
    A "slug" is a URL-friendly version of text. For example:
    "Hello World!" becomes "hello-world"
    "C++ Programming" becomes "c-programming"
    
    This is useful for creating anchor links from heading text.
    
    Args:
        text: Text to slugify (e.g., a heading like "My Great Post")
        
    Returns:
        Lowercase, hyphenated slug (e.g., "my-great-post")
    """
    # Step 1: Convert everything to lowercase
    # "Hello World!" → "hello world!"
    slug = text.lower()
    
    # Step 2: Remove special characters, keeping only letters, numbers, spaces, and hyphens
    # REGEX: [^\w\s-] means "match anything that is NOT a word char, space, or hyphen"
    # Then replace those matches with empty string (delete them)
    # "hello world!" → "hello world"
    slug = re.sub(r'[^\w\s-]', '', slug)
    
    # Step 3: Replace all spaces and multiple hyphens with a single hyphen
    # REGEX: [-\s]+ means "one or more hyphens or spaces"
    # "hello  world" → "hello-world"
    # "hello---world" → "hello-world"
    slug = re.sub(r'[-\s]+', '-', slug)
    
    # Step 4: Remove any leading or trailing hyphens
    # "-hello-world-" → "hello-world"
    return slug.strip('-')


def parse_footnotes(md_text: str) -> tuple[str, dict[str, str]]:
    """Parse footnotes from Markdown text.
    
    Footnotes let you add references without cluttering the main text.
    
    Syntax:
        Here's text with footnote[^1].
        
        [^1]: This is the footnote content.
    
    Args:
        md_text: Markdown text with potential footnotes
        
    Returns:
        Tuple of (text_without_footnote_defs, footnotes_dict)
        where footnotes_dict maps footnote IDs to their text
    """
    # === REGEX PATTERN EXPLANATION ===
    # This matches footnote definitions like: [^1]: Footnote text here
    #
    # Pattern breakdown:
    # ^          = Start of line
    # \[\^       = Literal "[^" (escaped because [ and ^ are special in regex)
    # (\w+)      = Capture group 1: footnote ID (letters/numbers, like "1" or "note1")
    # \]:        = Literal "]:" (closing bracket, colon)
    # \s*        = Optional whitespace
    # (.+)$      = Capture group 2: footnote text (everything to end of line)
    footnote_pattern = r'^\[\^(\w+)\]:\s*(.+)$'
    footnotes = {}
    lines = []
    
    # Go through each line, separating footnote definitions from content
    for line in md_text.split('\n'):
        match = re.match(footnote_pattern, line)
        if match:
            # This line is a footnote definition - save it to our dict
            footnote_id = match.group(1)     # e.g., "1" or "note1"
            footnote_text = match.group(2).strip()  # e.g., "This is the footnote"
            footnotes[footnote_id] = footnote_text
        else:
            # This line is regular content - keep it
            lines.append(line)
    
    return '\n'.join(lines), footnotes


def render_footnotes(footnotes: dict[str, str]) -> str:
    """Render footnotes as HTML.
    
    Args:
        footnotes: Dictionary mapping footnote IDs to their text
        
    Returns:
        HTML div with footnotes list, or empty string if no footnotes
    """
    if not footnotes:
        return ""
    
    html_lines = ['<div class="footnotes">', '  <hr>', '  <ol>']
    
    for key, text in sorted(footnotes.items()):
        html_lines.append(
            f'    <li id="fn-{key}">{text} <a href="#fnref-{key}" class="footnote-backref">↩</a></li>'
        )
    
    html_lines.extend(['  </ol>', '</div>'])
    
    return '\n'.join(html_lines)


def replace_footnote_refs(html: str) -> str:
    """Replace footnote reference markers with HTML links.
    
    Converts [^1] to <sup><a href="#fn-1" id="fnref-1">[1]</a></sup>
    
    Args:
        html: HTML content with [^n] markers
        
    Returns:
        HTML with footnote references as superscript links
    """
    def replace_fn_ref(match):
        footnote_id = match.group(1)
        return f'<sup><a href="#fn-{footnote_id}" id="fnref-{footnote_id}" class="footnote-ref">[{footnote_id}]</a></sup>'
    
    return re.sub(r'\[\^(\w+)\]', replace_fn_ref, html)