"""
Directive validator - checks directive syntax, usage, and performance.

Validates:
- Directive syntax is well-formed
- Required directive options present
- Tab markers properly formatted
- Nesting depth reasonable
- Performance warnings for directive-heavy pages
"""

from __future__ import annotations

import re
from collections import defaultdict
from pathlib import Path
from typing import TYPE_CHECKING, Any, override

from bengal.health.base import BaseValidator
from bengal.health.report import CheckResult
from bengal.rendering.parsers.factory import ParserFactory
from bengal.rendering.plugins.directives.validator import DirectiveSyntaxValidator

if TYPE_CHECKING:
    from bengal.core.site import Site


class DirectiveValidator(BaseValidator):
    """
    Validates directive syntax and usage across the site.

    Checks:
    - Directive blocks are well-formed (opening and closing)
    - Required options are present
    - Tab markers are properly formatted
    - Nesting depth is reasonable
    - Performance warnings for heavy directive usage
    """

    name = "Directives"
    description = "Validates directive syntax, completeness, and performance"
    enabled_by_default = True

    # Directive types we know about
    # This should match all directive types registered in bengal/rendering/plugins/directives/
    KNOWN_DIRECTIVES = {
        # Admonitions
        "note",
        "tip",
        "warning",
        "danger",
        "error",
        "info",
        "example",
        "success",
        "caution",
        # Badges
        "badge",
        "bdg",  # Alias for badge (Sphinx-Design compatibility)
        # Buttons
        "button",
        # Cards
        "card",
        "cards",
        "grid",  # Sphinx-Design compatibility
        # Tabs
        "tabs",
        "tab-set",
        "tab-item",
        # Code tabs
        "code-tabs",
        "code_tabs",
        # Dropdowns
        "dropdown",
        "details",
        # Tables
        "list-table",
        "data-table",
        "data_table",  # Alternative naming
        # Checklists
        "checklist",
        # Steps
        "steps",
        "step",
        # Rubric
        "rubric",
        # Includes
        "include",
        "literalinclude",
        # Marimo (if installed)
        "marimo",
    }

    # Admonition types (should use colon fences)
    ADMONITION_TYPES = {
        "note",
        "tip",
        "warning",
        "danger",
        "error",
        "info",
        "example",
        "success",
        "caution",
    }

    # Directives that are code-related and can reasonably use backtick fences
    CODE_BLOCK_DIRECTIVES = {
        "code-tabs",
        "code_tabs",
        "literalinclude",
    }

    # Performance thresholds
    MAX_DIRECTIVES_PER_PAGE = 10  # Warn if page has more than this
    MAX_NESTING_DEPTH = 5  # Warn if nesting deeper than this
    MAX_TABS_PER_BLOCK = 10  # Warn if single tabs block has more than this

    @override
    def validate(self, site: Site) -> list[CheckResult]:
        """Run directive validation checks."""
        results = []

        # Gather all directive data from source files
        directive_data = self._analyze_directives(site)

        # Check 1: Syntax validation
        results.extend(self._check_directive_syntax(directive_data))

        # Check 2: Completeness validation
        results.extend(self._check_directive_completeness(directive_data))

        # Check 3: Performance warnings
        results.extend(self._check_directive_performance(directive_data))

        # Check 4: Rendering validation (check output HTML)
        results.extend(self._check_directive_rendering(site, directive_data))

        return results

    def _is_autodoc_file(self, page: Any) -> bool:
        """
        Check if a page is autodoc-generated.

        Autodoc files are generated from source code and have:
        - Frontmatter with type: python-module, python-class, python-function, python-method, cli-reference, etc.
        - Source file references (source_file: path/to/source.py)
        - Generator metadata (generator: bengal-autodoc)
        - "Generated by Bengal autodoc" footer text
        - Can be in api/, cli/, or any directory

        Args:
            page: Page object to check

        Returns:
            True if this is an autodoc-generated file
        """
        # Check frontmatter for autodoc indicators
        frontmatter = getattr(page, "frontmatter", {})
        if isinstance(frontmatter, dict):
            # Autodoc files have:
            # 1. Type starting with "python-" (python-module, python-class, etc.)
            # 2. Type "cli-reference" (CLI autodoc)
            # 3. source_file field (indicates generated from source)
            # 4. generator: "bengal-autodoc" metadata
            page_type = frontmatter.get("type", "")
            if (
                page_type.startswith("python-")
                or page_type == "cli-reference"
                or "source_file" in frontmatter
                or frontmatter.get("generator") == "bengal-autodoc"
            ):
                return True

        # Check content for autodoc footer
        content = getattr(page, "content", "")
        # Autodoc files have "Generated by Bengal autodoc" footer
        return isinstance(content, str) and (
            "Generated by Bengal autodoc" in content or "Generated by Bengal" in content
        )

    def _analyze_directives(self, site: Site) -> dict[str, Any]:
        """
        Analyze all directives in site source files.

        Returns:
            Dictionary with directive statistics and issues
        """
        data = {
            "total_directives": 0,
            "by_type": defaultdict(int),
            "by_page": defaultdict(list),
            "syntax_errors": [],
            "completeness_errors": [],
            "performance_warnings": [],
            "fence_nesting_warnings": [],
        }

        # Analyze each page's source content
        for page in site.pages:
            if not page.source_path or not page.source_path.exists():
                continue

            # Skip generated pages (they don't have markdown source)
            if page.metadata.get("_generated"):
                continue

            # Skip autodoc-generated pages (API/CLI docs)
            if self._is_autodoc_file(page):
                continue

            try:
                content = page.source_path.read_text(encoding="utf-8")

                # Check for fence nesting structure using the shared validator
                fence_errors = DirectiveSyntaxValidator.validate_nested_fences(
                    content, page.source_path
                )
                for error in fence_errors:
                    # Extract line number if present
                    line_match = re.match(r"Line (\d+):", error)
                    line_num = int(line_match.group(1)) if line_match else 0

                    data["fence_nesting_warnings"].append(
                        {
                            "page": page.source_path,
                            "line": line_num,
                            "type": "structure",
                            "warning": error,
                        }
                    )

                # Check for markdown code blocks with nested code blocks (same fence length)
                code_block_warnings = self._check_code_block_nesting(content, page.source_path)
                for warning in code_block_warnings:
                    data["fence_nesting_warnings"].append(warning)

                page_directives = self._extract_directives(content, page.source_path)

                for directive in page_directives:
                    data["total_directives"] += 1
                    data["by_type"][directive["type"]] += 1
                    data["by_page"][str(page.source_path)].append(directive)

                    # Check for syntax errors
                    if directive.get("syntax_error"):
                        data["syntax_errors"].append(
                            {
                                "page": page.source_path,
                                "line": directive["line_number"],
                                "type": directive["type"],
                                "error": directive["syntax_error"],
                            }
                        )

                    # Check for completeness errors
                    if directive.get("completeness_error"):
                        data["completeness_errors"].append(
                            {
                                "page": page.source_path,
                                "line": directive["line_number"],
                                "type": directive["type"],
                                "error": directive["completeness_error"],
                            }
                        )

                    # Check for fence nesting warnings
                    if directive.get("fence_nesting_warning"):
                        data["fence_nesting_warnings"].append(
                            {
                                "page": page.source_path,
                                "line": directive["line_number"],
                                "type": directive["type"],
                                "warning": directive["fence_nesting_warning"],
                            }
                        )

                    # Check for fence style warnings (backtick vs colon)
                    if directive.get("fence_style_warning"):
                        data["fence_nesting_warnings"].append(
                            {
                                "page": page.source_path,
                                "line": directive["line_number"],
                                "type": directive["type"],
                                "warning": directive["fence_style_warning"],
                            }
                        )

            except Exception:
                # Skip files we can't read
                pass

        # Check for performance issues
        for page_path, directives in data["by_page"].items():
            # Too many directives on one page?
            if len(directives) > self.MAX_DIRECTIVES_PER_PAGE:
                data["performance_warnings"].append(
                    {
                        "page": Path(page_path),
                        "issue": "heavy_directive_usage",
                        "count": len(directives),
                        "message": f"{len(directives)} directives on one page (>{self.MAX_DIRECTIVES_PER_PAGE})",
                    }
                )

            # Check individual directive issues
            for directive in directives:
                # Too many tabs in a tabs block?
                if (
                    directive["type"] == "tabs"
                    and directive.get("tab_count", 0) > self.MAX_TABS_PER_BLOCK
                ):
                    data["performance_warnings"].append(
                        {
                            "page": Path(page_path),
                            "issue": "too_many_tabs",
                            "line": directive["line_number"],
                            "count": directive["tab_count"],
                            "message": f"Tabs block has {directive['tab_count']} tabs (>{self.MAX_TABS_PER_BLOCK})",
                        }
                    )

        return data

    def _is_inside_code_block(self, content: str, position: int) -> bool:
        """
        Check if a position in content is inside a markdown code block.

        Args:
            content: Full markdown content
            position: Character position to check

        Returns:
            True if position is inside a code block (```...```, ~~~...~~~, or indented 4+ spaces)
        """
        # Find all code block boundaries
        code_block_pattern = r"^(`{3,}|~{3,})[^\n]*$"
        lines = content[:position].split("\n")

        # Track if we're inside a fenced code block
        in_fenced_block = False
        code_block_marker = None

        # Track if we're in an indented code block (4+ spaces)
        in_indented_block = False

        for i, line in enumerate(lines):
            # Check if this line is a code block fence
            match = re.match(code_block_pattern, line)
            if match:
                marker = match.group(1)
                # If we're in a code block and this matches the opening marker, close it
                if in_fenced_block and marker == code_block_marker:
                    in_fenced_block = False
                    code_block_marker = None
                # Otherwise, open a new code block
                else:
                    in_fenced_block = True
                    code_block_marker = marker
                in_indented_block = False  # Fenced blocks override indented
            else:
                # Check for indented code blocks (4+ spaces, not a list item)
                # A line with 4+ spaces that's not a list item is likely a code block
                stripped = line.lstrip()
                indent = len(line) - len(stripped)
                # If line has 4+ spaces and is not empty, it's likely code
                if indent >= 4 and stripped:
                    # Check if previous line was also indented (continuation) or if this starts a block
                    if i == 0 or (
                        lines[i - 1].strip() and len(lines[i - 1]) - len(lines[i - 1].lstrip()) >= 4
                    ):
                        in_indented_block = True
                    elif in_indented_block:
                        # Continue in indented block
                        in_indented_block = True
                    else:
                        in_indented_block = False
                else:
                    # Empty line or non-indented line - check if we should continue indented block
                    # (some markdown parsers allow empty lines in indented code blocks)
                    if in_indented_block and not stripped:
                        # Empty line, might still be in code block
                        continue
                    else:
                        in_indented_block = False

        return in_fenced_block or in_indented_block

    def _check_code_block_nesting(self, content: str, file_path: Path) -> list[dict[str, Any]]:
        """
        Check for markdown code blocks that contain nested code blocks with the same fence length.

        Example issue:
            ```markdown
            ```python
            code here
            ```
            ```

        The outer block should use 4+ backticks to avoid conflicts.

        Returns:
            List of warning dictionaries
        """
        warnings = []
        lines = content.split("\n")

        # Pattern for code block fences: ```language or ```language:path or just ```
        # Note: ```{directive} is a directive, not a code block - skip those
        code_block_pattern = re.compile(r"^(\s*)(`{3,})(\w*)(?::[^\s]*)?\s*$")
        directive_pattern = re.compile(r"^(\s*)(`{3,})\{([^}]+)\}")  # ```{directive}
        stack: list[tuple[int, int, str]] = []  # (line_num, fence_length, language)
        directive_stack: list[
            int
        ] = []  # Track directive fence lengths to skip their closing fences

        for i, line in enumerate(lines, 1):
            # Check if this is a directive (```{name}) - directives are handled separately
            directive_match = directive_pattern.match(line)
            if directive_match:
                # Track the directive's fence length so we can skip its closing fence
                directive_fence_length = len(directive_match.group(2))
                directive_stack.append(directive_fence_length)
                continue

            match = code_block_pattern.match(line)
            if match:
                indent = len(match.group(1))
                fence_marker = match.group(2)
                language = match.group(3)
                fence_length = len(fence_marker)

                # If this is a closing fence and we have directives on the stack, check if it closes a directive
                if not language and directive_stack and fence_length == directive_stack[-1]:
                    # This closes a directive, not a code block - skip it
                    directive_stack.pop()
                    continue

                # Skip if indented 4+ spaces (likely inside another code block example)
                if indent >= 4:
                    continue

                # Skip if inside a colon directive (:::{name}) - those are handled separately
                char_pos = len("\n".join(lines[: i - 1]))
                if self._is_inside_colon_directive(content, char_pos):
                    continue

                # Check if this closes a code block
                # In markdown: ```language opens a new block, ``` (no language) closes a block
                if stack:
                    top_line, top_length, top_lang = stack[-1]

                    # If this is a closing fence (no language) and length matches, it closes the top block
                    if not language and fence_length == top_length:
                        stack.pop()
                    # If fence length is longer, it might close a parent (but this is unusual)
                    elif fence_length > top_length:
                        # This shouldn't happen with code blocks, but handle it
                        stack.pop()
                    # If this is an opening fence (has language) and length matches parent, it's nested with same length = PROBLEM
                    elif language and fence_length == top_length:
                        warnings.append(
                            {
                                "page": file_path,
                                "line": i,
                                "type": "structure",
                                "warning": (
                                    f"Line {i}: Code block with {fence_length} backticks contains nested code block "
                                    f"with same fence length (line {top_line}). "
                                    f"Fix: Change outer block to use {fence_length + 1}+ backticks (e.g., ````{top_lang or 'markdown'}`)."
                                ),
                            }
                        )
                        stack.append((i, fence_length, language))
                    # If fence length is shorter, it's nested - this is fine
                    elif fence_length < top_length:
                        # Nested block with shorter fence - this is fine
                        stack.append((i, fence_length, language))
                    else:
                        # Opening a new block (different length, or no parent)
                        stack.append((i, fence_length, language))
                else:
                    # Opening a new code block
                    stack.append((i, fence_length, language))

        return warnings

    def _is_inside_colon_directive(self, content: str, position: int) -> bool:
        """
        Check if a position is inside a colon directive (:::{name}).
        This is separate from code blocks - we want to check code blocks even if they're inside directives.
        """
        lines = content[:position].split("\n")
        colon_pattern = re.compile(r"^(\s*)(:{3,})\{([^}]+)\}")
        closing_pattern = re.compile(r"^(\s*)(:{3,})\s*$")

        in_directive = False
        directive_depth = 0

        for line in lines:
            if colon_pattern.match(line):
                directive_depth += 1
                in_directive = True
            elif closing_pattern.match(line):
                directive_depth -= 1
                if directive_depth == 0:
                    in_directive = False

        return in_directive and directive_depth > 0

    def _is_inside_inline_code(self, content: str, position: int) -> bool:
        """
        Check if a position in content is inside inline code (single backticks).

        Args:
            content: Full markdown content
            position: Character position to check

        Returns:
            True if position is inside inline code (`...`)
        """
        # Get the line containing this position
        lines_before = content[:position].split("\n")
        if not lines_before:
            return False

        current_line = lines_before[-1]

        # Count backticks before the position on this line
        # Odd number means we're inside inline code
        char_pos_in_line = len(current_line)
        backticks_before = current_line[:char_pos_in_line].count("`")

        # Also check if we're inside a code block (which would also mean we're "inside code")
        if self._is_inside_code_block(content, position):
            return True

        # Odd number of backticks means we're inside inline code
        return backticks_before % 2 == 1

    def _extract_directives(self, content: str, file_path: Path) -> list[dict[str, Any]]:
        """
        Extract all directive blocks from markdown content (colon fences only).

        Note: Backtick fences are no longer supported - only colon fences (:::) are accepted.
        This avoids conflicts when directives appear in code examples.

        Skips directives that are inside markdown code blocks (```...```) to avoid false positives
        from documentation examples.

        Args:
            content: Markdown content
            file_path: Path to file (for error reporting)

        Returns:
            List of directive dictionaries with metadata
        """
        directives = []

        # STRICT: Only colon fences are supported - backticks reserved for code blocks
        # Pattern: Colon fences: :{3,}\{directive_type} optional_title
        #          :option: value
        #
        #          content
        #          :{3,}
        # Note: Colon fences can be indented, but closing fence colon count must match
        # We match the opening fence, then find the matching closing fence by colon count
        # IMPORTANT: Must have at least 3 colons (:::) to avoid matching definition lists (:   )
        colon_start_pattern = r"^(\s*)(:{3,})\{(\w+(?:-\w+)?)\}([^\n]*)"
        lines = content.split("\n")
        i = 0
        while i < len(lines):
            match = re.match(colon_start_pattern, lines[i])
            if match:
                # Skip if line is indented 4+ spaces (likely inside a code block example)
                indent = len(match.group(1))
                if indent >= 4:
                    i += 1
                    continue

                # Calculate character position to check if inside code block or inline code
                char_pos = len("\n".join(lines[:i]))
                # Skip if inside a code block or inline code (it's an example, not a real directive)
                if self._is_inside_code_block(content, char_pos) or self._is_inside_inline_code(
                    content, char_pos
                ):
                    i += 1
                    continue
                fence_marker = match.group(2)  # e.g., ::: or ::::
                directive_type = match.group(3)
                title = match.group(4).strip()
                fence_depth = len(fence_marker)

                # Find the matching closing fence (same colon count, optional indent)
                directive_content_lines = []
                j = i + 1
                found_closing = False
                while j < len(lines):
                    # Check if this line is the closing fence (same colon count)
                    closing_pattern = rf"^\s*:{fence_depth}\s*$"
                    if re.match(closing_pattern, lines[j]):
                        found_closing = True
                        break
                    directive_content_lines.append(lines[j])
                    j += 1

                if not found_closing:
                    # Unclosed fence - will be caught by validate_nested_fences
                    i += 1
                    continue

                directive_content = "\n".join(directive_content_lines)
                line_number = i + 1

                directive_info = {
                    "type": directive_type,
                    "title": title,
                    "content": directive_content,
                    "line_number": line_number,
                    "file_path": file_path,
                    "fence_depth": fence_depth,
                    "fence_type": "colon",
                }

                # Check for fence nesting issues
                self._check_fence_nesting(directive_info)

                # Check if directive type is known
                if directive_type not in self.KNOWN_DIRECTIVES:
                    directive_info["syntax_error"] = f"Unknown directive type: {directive_type}"

                # Validate specific directive types
                if directive_type == "tabs":
                    self._validate_tabs_directive(directive_info)
                elif directive_type in ("code-tabs", "code_tabs"):
                    self._validate_code_tabs_directive(directive_info)
                elif directive_type in ("dropdown", "details"):
                    self._validate_dropdown_directive(directive_info)

                directives.append(directive_info)
                i = j + 1  # Skip past closing fence
            else:
                i += 1

        return directives

    def _validate_tabs_directive(self, directive: dict[str, Any]) -> None:
        """Validate tabs directive content."""
        content = directive["content"]

        # Check for tab markers: ### Tab: Title
        tab_markers = re.findall(r"^### Tab: (.+)$", content, re.MULTILINE)
        directive["tab_count"] = len(tab_markers)

        if len(tab_markers) == 0:
            # Check if there's a malformed marker
            bad_markers = re.findall(r"^###\s*Ta[^b]", content, re.MULTILINE)
            if bad_markers:
                directive["syntax_error"] = 'Malformed tab marker (use "### Tab: Title")'
            else:
                # Single tab or no tabs - might be intentional, just note it
                directive["completeness_error"] = (
                    "Tabs directive has no tab markers (### Tab: Title)"
                )
        elif len(tab_markers) == 1:
            directive["completeness_error"] = (
                "Tabs directive has only 1 tab (consider using admonition instead)"
            )

        # Check if content is empty
        if not content.strip():
            directive["completeness_error"] = "Tabs directive has no content"

    def _validate_code_tabs_directive(self, directive: dict[str, Any]) -> None:
        """Validate code-tabs directive content."""
        content = directive["content"]

        # Check for tab markers
        tab_markers = re.findall(r"^### Tab: (.+)$", content, re.MULTILINE)
        directive["tab_count"] = len(tab_markers)

        if len(tab_markers) == 0:
            directive["completeness_error"] = (
                "Code-tabs directive has no tab markers (### Tab: Language)"
            )

        # Check if content is empty
        if not content.strip():
            directive["completeness_error"] = "Code-tabs directive has no content"

    def _validate_dropdown_directive(self, directive: dict[str, Any]) -> None:
        """Validate dropdown directive content."""
        content = directive["content"]

        # Check if content is empty
        if not content.strip():
            directive["completeness_error"] = "Dropdown directive has no content"

        # Check if title is missing
        if not directive["title"]:
            # Title is optional but recommended
            pass

    def _check_fence_nesting(self, directive: dict[str, Any]) -> None:
        """
        Check for fence nesting issues - when a directive uses 3 backticks
        but contains code blocks that also use 3 backticks.

        This checks if:
        1. The directive uses exactly 3 backticks
        2. The content appears truncated (suspiciously short) OR
        3. The content contains code block markers

        Args:
            directive: Directive info dict with 'fence_depth' and 'content'
        """
        content = directive["content"]
        fence_depth = directive["fence_depth"]

        # Only check if using exactly 3 backticks for the directive
        if fence_depth != 3:
            return

        # Check 1: Look for code blocks in the extracted content
        # Match both ``` and ~~~ fenced code blocks
        code_block_pattern = r"^(`{3,}|~{3,})[a-zA-Z0-9_-]*\s*$"

        lines = content.split("\n")
        has_code_blocks = False
        for line in lines:
            match = re.match(code_block_pattern, line.strip())
            if match:
                fence_marker = match.group(1)
                # If we find a 3-backtick code block inside a 3-backtick directive
                if fence_marker.startswith("`") and len(fence_marker) == 3:
                    has_code_blocks = True
                    break

        if has_code_blocks:
            directive["fence_nesting_warning"] = (
                "Directive uses 3 backticks (```) but contains 3-backtick code blocks. "
                "Use 4+ backticks (````) for the directive to avoid parsing issues."
            )
            return

        # Check 2: Detect suspiciously short content for tabs/code-tabs directives
        # These directives typically have substantial content; if truncated, it's a red flag
        directive_type = directive["type"]
        if directive_type in ("tabs", "code-tabs", "code_tabs"):
            # Count tab markers
            tab_count = len(re.findall(r"^### Tab:", content, re.MULTILINE))
            content_lines = len([line for line in lines if line.strip()])

            # If we have tab markers but very little content, might be truncated
            if tab_count > 0 and content_lines < (tab_count * 3):
                # Probably truncated - warn about potential nesting
                directive["fence_nesting_warning"] = (
                    f"Directive content appears incomplete ({content_lines} lines, {tab_count} tabs). "
                    f"If tabs contain code blocks, use 4+ backticks (````) for the directive fence."
                )

    def _get_line_with_context(
        self, file_path: Path, line_number: int, context_lines: int = 2
    ) -> str:
        """
        Get a line from a file with context (lines before/after).

        Returns formatted string with line numbers and content, suitable for copy-paste to AI.
        """
        try:
            if not file_path.exists():
                return f"Line {line_number}: (file not found)"

            content = file_path.read_text(encoding="utf-8")
            lines = content.split("\n")

            # Calculate range
            start = max(0, line_number - context_lines - 1)
            end = min(len(lines), line_number + context_lines)

            # Build output
            result_lines = []
            for i in range(start, end):
                line_num = i + 1
                marker = ">>>" if line_num == line_number else "   "
                result_lines.append(f"{marker} {line_num:4d} | {lines[i]}")

            return "\n".join(result_lines)
        except Exception:
            return f"Line {line_number}: (could not read file)"

    def _check_directive_syntax(self, data: dict[str, Any]) -> list[CheckResult]:
        """Check directive syntax is valid."""
        results = []
        errors = data["syntax_errors"]
        fence_warnings = data["fence_nesting_warnings"]

        # Check for syntax errors
        if errors:
            # Build detailed error messages with context
            details = []
            for e in errors[:5]:  # Limit to first 5 for readability
                file_path = e["page"]
                line_num = e["line"]
                error_msg = f"{file_path.name}:{line_num} - {e['type']}: {e['error']}"

                # Add context snippet
                context = self._get_line_with_context(file_path, line_num)
                details.append(f"{error_msg}\n{context}")

            results.append(
                CheckResult.error(
                    f"{len(errors)} directive(s) have syntax errors",
                    recommendation="Fix directive syntax. Check directive names and closing backticks.",
                    details=details,
                )
            )
        elif data["total_directives"] > 0:
            results.append(
                CheckResult.success(
                    f"All {data['total_directives']} directive(s) syntactically valid"
                )
            )
        else:
            # No directives found
            results.append(CheckResult.success("No directives found in site (validation skipped)"))

        # Check for fence nesting warnings
        if fence_warnings:
            # Group by file for better organization
            file_groups = {}
            # Metadata for autofix (full structured data)
            metadata_warnings = []

            for w in fence_warnings:
                file_key = w["page"].name
                if file_key not in file_groups:
                    file_groups[file_key] = []
                file_groups[file_key].append(w)

                # Add to metadata (convert Path to str for serialization)
                w_copy = w.copy()
                w_copy["page"] = str(w["page"])
                metadata_warnings.append(w_copy)

            # Build details with file grouping and context
            details = []
            for file_name, warnings in list(file_groups.items())[:3]:
                # Show first warning with context, then list others
                first_warning = warnings[0]
                file_path = first_warning["page"]
                line_num = first_warning["line"]

                detail_msg = f"{file_name}:{line_num} - {first_warning.get('warning', 'fence nesting issue')}"
                context = self._get_line_with_context(file_path, line_num)
                details.append(f"{detail_msg}\n{context}")

                # Add other line numbers if multiple issues in same file
                if len(warnings) > 1:
                    other_lines = sorted(set(w["line"] for w in warnings[1:]))
                    details.append(
                        f"  ... and {len(warnings) - 1} more issue(s) at lines: {', '.join(map(str, other_lines))}"
                    )

            if len(fence_warnings) > 3:
                details.append(
                    f"... and {len(fence_warnings) - sum(len(w) for w in list(file_groups.values())[:3])} more"
                )

            results.append(
                CheckResult.warning(
                    f"{len(fence_warnings)} directive(s) have fence nesting issues",
                    recommendation=(
                        "These directives use 3 backticks (```) but contain code blocks with 3 backticks. "
                        "Fix: Change directive opening from ```{name} to ````{name} (use 4+ backticks)."
                    ),
                    details=details,
                    metadata={"fence_warnings": metadata_warnings},
                )
            )

        return results

    def _check_directive_completeness(self, data: dict[str, Any]) -> list[CheckResult]:
        """Check directives are complete (have required content, options, etc)."""
        results = []
        errors = data["completeness_errors"]

        if errors:
            # Separate warnings from errors
            # Single-tab or empty content are warnings, not hard errors
            warning_keywords = ["only 1 tab", "consider using", "has no tab markers"]
            warnings = [e for e in errors if any(kw in e["error"] for kw in warning_keywords)]
            hard_errors = [e for e in errors if e not in warnings]

            if hard_errors:
                details = []
                for e in hard_errors[:5]:
                    file_path = e["page"]
                    line_num = e["line"]
                    error_msg = f"{file_path.name}:{line_num} - {e['type']}: {e['error']}"
                    context = self._get_line_with_context(file_path, line_num)
                    details.append(f"{error_msg}\n{context}")

                results.append(
                    CheckResult.error(
                        f"{len(hard_errors)} directive(s) incomplete",
                        recommendation="Fix incomplete directives. Add required content and options.",
                        details=details,
                    )
                )

            if warnings:
                details = []
                for e in warnings[:5]:
                    file_path = e["page"]
                    line_num = e["line"]
                    error_msg = f"{file_path.name}:{line_num} - {e['type']}: {e['error']}"
                    context = self._get_line_with_context(file_path, line_num)
                    details.append(f"{error_msg}\n{context}")

                results.append(
                    CheckResult.warning(
                        f"{len(warnings)} directive(s) could be improved",
                        recommendation="Review directive usage. Consider simpler alternatives for single-item directives.",
                        details=details,
                    )
                )

        if not errors and data["total_directives"] > 0:
            results.append(
                CheckResult.success(f"All {data['total_directives']} directive(s) complete")
            )

        return results

    def _check_directive_performance(self, data: dict[str, Any]) -> list[CheckResult]:
        """Check for performance issues with directive usage."""
        results = []
        warnings = data["performance_warnings"]

        if warnings:
            # Group by issue type
            heavy_pages = [w for w in warnings if w["issue"] == "heavy_directive_usage"]
            too_many_tabs = [w for w in warnings if w["issue"] == "too_many_tabs"]

            if heavy_pages:
                results.append(
                    CheckResult.warning(
                        f"{len(heavy_pages)} page(s) have heavy directive usage (>{self.MAX_DIRECTIVES_PER_PAGE} directives)",
                        recommendation="Consider splitting large pages or reducing directive nesting. Each directive adds ~20-50ms build time.",
                        details=[
                            f"{w['page'].name}: {w['count']} directives"
                            for w in sorted(heavy_pages, key=lambda x: x["count"], reverse=True)[:5]
                        ],
                    )
                )

            if too_many_tabs:
                results.append(
                    CheckResult.warning(
                        f"{len(too_many_tabs)} tabs block(s) have many tabs (>{self.MAX_TABS_PER_BLOCK})",
                        recommendation="Consider splitting into multiple tabs blocks or separate pages. Large tabs blocks slow rendering.",
                        details=[
                            f"{w['page'].name}:{w['line']}: {w['count']} tabs"
                            for w in sorted(too_many_tabs, key=lambda x: x["count"], reverse=True)[
                                :5
                            ]
                        ],
                    )
                )

        # Always show statistics if there are directives
        if data["total_directives"] > 0:
            top_types = sorted(data["by_type"].items(), key=lambda x: x[1], reverse=True)[:3]
            type_summary = ", ".join([f"{t}({c})" for t, c in top_types])
            avg_per_page = data["total_directives"] / max(len(data["by_page"]), 1)

            results.append(
                CheckResult.info(
                    f"Directive usage: {data['total_directives']} total across {len(data['by_page'])} pages. "
                    f"Most used: {type_summary}. Average per page: {avg_per_page:.1f}"
                )
            )

        return results

    def _check_directive_rendering(self, site: Site, data: dict[str, Any]) -> list[CheckResult]:
        """Check that directives rendered properly in output HTML."""
        results = []
        issues = []

        # Sample pages (check all non-generated pages)
        pages_to_check = [
            p
            for p in site.pages
            if p.output_path and p.output_path.exists() and not p.metadata.get("_generated")
        ]

        for page in pages_to_check:
            try:
                content = page.output_path.read_text(encoding="utf-8")

                # Check for unrendered directive markers (outside code blocks)
                if self._has_unrendered_directives(content):
                    issues.append(
                        f"{page.output_path.name}: Directive syntax error - directive not rendered"
                    )

                # Check for directive parsing error markers
                if 'class="markdown-error"' in content:
                    issues.append(f"{page.output_path.name}: Directive parsing error in output")

            except Exception:
                pass

        if issues:
            # Group by file for cleaner output
            file_counts = {}
            for issue in issues:
                file_name = issue.split(":")[0]
                file_counts[file_name] = file_counts.get(file_name, 0) + 1

            details = []
            for file_name, count in list(file_counts.items())[:3]:
                details.append(f"{file_name}: {count} issue(s)")
            remaining = len(issues) - sum(list(file_counts.values())[:3])
            if remaining > 0:
                details.append(f"... and {remaining} more")

            results.append(
                CheckResult.error(
                    f"{len(issues)} page(s) have directive rendering errors",
                    recommendation=(
                        "Directives failed to render. Common causes: "
                        "missing closing fence, invalid syntax, or unknown directive type. "
                        "Check the directive syntax in the source markdown files."
                    ),
                    details=details,
                )
            )
        elif data["total_directives"] > 0:
            results.append(
                CheckResult.success(
                    f"All directive(s) rendered successfully (checked {len(pages_to_check)} pages)"
                )
            )

        return results

    def _has_unrendered_directives(self, html_content: str) -> bool:
        """
        Check if HTML has unrendered directive blocks (outside code blocks).

        Distinguishes between:
        - Actual unrendered directives (bad)
        - Documented/escaped directive syntax in code examples (ok)

        Args:
            html_content: HTML content to check

        Returns:
            True if unrendered directives found (not in code blocks)
        """
        try:
            parser = ParserFactory.get_html_parser("native")
            soup = parser(html_content)
            remaining_text = soup.get_text()
            return bool(re.search(r"```\{(\w+)", remaining_text))
        except Exception:
            return re.search(r"```\{(\w+)", html_content) is not None
