"""
Rendering Pipeline - Orchestrates the parsing, AST building, templating, and output rendering.
"""

from __future__ import annotations

import html as html_module
import re
import threading
from pathlib import Path
from typing import Any

from bengal.core.page import Page
from bengal.rendering.parsers import BaseMarkdownParser, create_markdown_parser
from bengal.rendering.renderer import Renderer
from bengal.rendering.template_engine import TemplateEngine
from bengal.utils.logger import get_logger, truncate_error
from bengal.utils.url_strategy import URLStrategy

logger = get_logger(__name__)


# Thread-local storage for parser instances (reuse parsers per thread)
_thread_local = threading.local()

# Cache for created directories (reduces syscalls in parallel builds)
_created_dirs = set()
_created_dirs_lock = threading.Lock()


def _get_thread_parser(engine: str | None = None) -> BaseMarkdownParser:
    """
    Get or create a MarkdownParser instance for the current thread.

    Thread-Local Caching Strategy:
        - Creates ONE parser per worker thread (expensive operation ~10ms)
        - Caches it for the lifetime of that thread
        - Each thread reuses its parser for all pages it processes
        - Total parsers created = number of worker threads

    Performance Impact:
        With max_workers=N (from config):
        - N worker threads created
        - N parser instances created (one per thread)
        - Each parser handles ~(total_pages / N) pages

        Example with max_workers=10 and 200 pages:
        - 10 threads → 10 parsers created
        - Each parser processes ~20 pages
        - Creation cost: 10ms × 10 = 100ms one-time
        - Reuse savings: 9.9 seconds (avoiding 190 × 10ms)

    Thread Safety:
        Each thread gets its own parser instance, no locking needed.
        Read-only access to site config and xref_index is safe.

    Args:
        engine: Parser engine to use ('python-markdown', 'mistune', or None for default)

    Returns:
        Cached MarkdownParser instance for this thread

    Note:
        If you see N parser instances created where N = max_workers,
        this is OPTIMAL behavior, not a bug!
    """
    # Store parser per engine type
    cache_key = f"parser_{engine or 'default'}"
    if not hasattr(_thread_local, cache_key):
        setattr(_thread_local, cache_key, create_markdown_parser(engine))
    return getattr(_thread_local, cache_key)


class RenderingPipeline:
    """
    Coordinates the entire rendering process for content.

    Pipeline stages:
    1. Parse source content (Markdown, etc.)
    2. Build Abstract Syntax Tree (AST)
    3. Apply templates
    4. Render output (HTML)
    5. Write to output directory
    """

    def __init__(
        self,
        site: Any,
        dependency_tracker: Any = None,
        quiet: bool = False,
        build_stats: Any = None,
        build_context: Any | None = None,
    ) -> None:
        """
        Initialize the rendering pipeline.

        Parser Selection:
            Reads from config in this order:
            1. config['markdown_engine'] (legacy)
            2. config['markdown']['parser'] (preferred)
            3. Default: 'mistune' (recommended for speed)

            Common values:
            - 'mistune': Fast parser, recommended for most sites (default)
            - 'python-markdown': Full-featured, slightly slower

        Parser Caching:
            Uses thread-local caching via _get_thread_parser().
            Creates ONE parser per worker thread, cached for reuse.

            With max_workers=N:
            - First page in thread: creates parser (~10ms)
            - Subsequent pages: reuses cached parser (~0ms)
            - Total parsers = N (optimal)

        Cross-Reference Support:
            If site has xref_index (built during discovery):
            - Enables [[link]] syntax in markdown
            - Enables automatic .md link resolution (future)
            - O(1) lookup performance

        Args:
            site: Site instance with config and xref_index
            dependency_tracker: Optional tracker for incremental builds
            quiet: If True, suppress per-page output
            build_stats: Optional BuildStats object to collect warnings

        Note:
            Each worker thread creates its own RenderingPipeline instance.
            The parser is cached at thread level, not pipeline level.
        """
        self.site = site
        # Get markdown engine from config (default: mistune)
        # Check both old location (markdown_engine) and new nested location (markdown.parser)
        markdown_engine = site.config.get("markdown_engine")
        if not markdown_engine:
            # Check nested markdown section
            markdown_config = site.config.get("markdown", {})
            markdown_engine = markdown_config.get("parser", "mistune")
        # Allow injection of parser via BuildContext for tests/experiments
        injected_parser = None
        if build_context and getattr(build_context, "markdown_parser", None):
            injected_parser = build_context.markdown_parser
        # Use thread-local parser to avoid re-initialization overhead
        self.parser = injected_parser or _get_thread_parser(markdown_engine)

        # Enable cross-references if xref_index is available
        if hasattr(site, "xref_index") and hasattr(self.parser, "enable_cross_references"):
            self.parser.enable_cross_references(site.xref_index)

        self.dependency_tracker = dependency_tracker
        self.quiet = quiet
        self.build_stats = build_stats
        # Allow injection of TemplateEngine via BuildContext (e.g., strict modes or mocks)
        if build_context and getattr(build_context, "template_engine", None):
            self.template_engine = build_context.template_engine
        else:
            self.template_engine = TemplateEngine(site)
        if self.dependency_tracker:
            self.template_engine._dependency_tracker = self.dependency_tracker
        self.renderer = Renderer(self.template_engine, build_stats=build_stats)
        # Optional build context for future DI (e.g., caches, reporters)
        self.build_context = build_context

    def process_page(self, page: Page) -> None:
        """
        Process a single page through the entire pipeline.

        Args:
            page: Page to process
        """
        # Track this page if we have a dependency tracker
        if self.dependency_tracker and not page.metadata.get("_generated"):
            self.dependency_tracker.start_page(page.source_path)

        # Stage 0: Determine output path early so page.url works correctly
        if not page.output_path:
            page.output_path = self._determine_output_path(page)

        # OPTIMIZATION #2: Try parsed content cache first
        # Skip markdown parsing if we have cached HTML and only template changed
        template = self._determine_template(page)
        parser_version = self._get_parser_version()

        if self.dependency_tracker and hasattr(self.dependency_tracker, "cache"):
            cache = self.dependency_tracker.cache
            if cache and not page.metadata.get("_generated"):
                cached = cache.get_parsed_content(
                    page.source_path, page.metadata, template, parser_version
                )

                if cached:
                    # Cache HIT - skip markdown parsing!
                    page.parsed_ast = cached["html"]
                    page.toc = cached["toc"]
                    page._toc_items_cache = cached.get("toc_items", [])

                    # Track cache hit for statistics
                    if self.build_stats:
                        if not hasattr(self.build_stats, "parsed_cache_hits"):
                            self.build_stats.parsed_cache_hits = 0
                        self.build_stats.parsed_cache_hits += 1

                    # Continue to template rendering (skipped parsing, will apply current template)
                    # Note: We don't return early - we need to apply the template
                    parsed_content = cached["html"]

                    # Transform internal links to include baseurl (for GitHub Pages, etc.)
                    parsed_content = self._transform_internal_links(parsed_content)

                    # Skip to stage 3 (template rendering)
                    page.extract_links()
                    html_content = self.renderer.render_content(parsed_content)
                    page.rendered_html = self.renderer.render_page(page, html_content)

                    # HTML formatting on cache-hit path as well
                    try:
                        from bengal.postprocess.html_output import format_html_output

                        if page.metadata.get("no_format") is True:
                            mode = "raw"
                            options = {}
                        else:
                            html_cfg = self.site.config.get("html_output", {}) or {}
                            mode = html_cfg.get(
                                "mode",
                                "minify" if self.site.config.get("minify_html", True) else "pretty",
                            )
                            options = {
                                "remove_comments": html_cfg.get(
                                    "remove_comments", mode == "minify"
                                ),
                                "collapse_blank_lines": html_cfg.get("collapse_blank_lines", True),
                            }
                        page.rendered_html = format_html_output(
                            page.rendered_html, mode=mode, options=options
                        )
                    except Exception:
                        pass

                    self._write_output(page)

                    if self.dependency_tracker and not page.metadata.get("_generated"):
                        self.dependency_tracker.end_page()

                    return  # EARLY RETURN - parsing skipped, template applied!

        # Stage 1 & 2: Parse content with variable substitution
        #
        # ARCHITECTURE: Clean separation of concerns
        # - Mistune parser: Handles {{ vars }} via VariableSubstitutionPlugin
        # - Templates: Handle {% if %}, {% for %}, complex logic
        # - Code blocks: Naturally stay literal (AST-level operation)
        #
        # Pages can disable preprocessing by setting `preprocess: false` in frontmatter.
        # This is useful for documentation pages that show template syntax examples.
        #
        # Decide whether TOC is needed. Skip TOC path if disabled or no headings present.
        need_toc = True
        if page.metadata.get("toc") is False:
            need_toc = False
        else:
            # Quick heuristic: only generate TOC if markdown likely contains h2-h4 headings
            # Matches atx-style (##, ###, ####) and setext-style ("---" underlines after a line)
            content_text = page.content or ""
            likely_has_atx = re.search(
                r"^(?:\s{0,3})(?:##|###|####)\s+.+", content_text, re.MULTILINE
            )
            if not likely_has_atx:
                # Lightweight check for setext h2 (===) and h3 (---) style underlines
                likely_has_setext = re.search(
                    r"^.+\n\s{0,3}(?:===+|---+)\s*$", content_text, re.MULTILINE
                )
                need_toc = bool(likely_has_setext)
            else:
                need_toc = True

        if hasattr(self.parser, "parse_with_toc_and_context"):
            # Mistune with VariableSubstitutionPlugin (recommended)
            # Check if preprocessing is disabled
            if page.metadata.get("preprocess") is False:
                if need_toc:
                    # Parse without variable substitution (for docs showing template syntax)
                    parsed_content, toc = self.parser.parse_with_toc(page.content, page.metadata)
                    # Escape raw template syntax so it doesn't leak into final HTML
                    parsed_content = self._escape_template_syntax_in_html(parsed_content)
                else:
                    parsed_content = self.parser.parse(page.content, page.metadata)
                    # Escape raw template syntax so it doesn't leak into final HTML
                    parsed_content = self._escape_template_syntax_in_html(parsed_content)
                    toc = ""
            else:
                # Single-pass parsing with variable substitution - fast and simple!
                context = {"page": page, "site": self.site, "config": self.site.config}
                if need_toc:
                    parsed_content, toc = self.parser.parse_with_toc_and_context(
                        page.content, page.metadata, context
                    )
                else:
                    parsed_content = self.parser.parse_with_context(
                        page.content, page.metadata, context
                    )
                    toc = ""
        else:
            # FALLBACK: python-markdown (legacy)
            # Uses Jinja2 preprocessing - deprecated, use Mistune instead
            content = self._preprocess_content(page)
            if need_toc and hasattr(self.parser, "parse_with_toc"):
                parsed_content, toc = self.parser.parse_with_toc(content, page.metadata)
            else:
                parsed_content = self.parser.parse(content, page.metadata)
                toc = ""

            # If preprocessing was explicitly disabled, ensure raw template markers are escaped
            if page.metadata.get("preprocess") is False:
                parsed_content = self._escape_template_syntax_in_html(parsed_content)

        # Additional hardening: ensure no Jinja2 block syntax leaks in HTML content
        # even when pages use variable substitution path (handled in MistuneParser as well).
        parsed_content = self._escape_jinja_blocks(parsed_content)

        # Transform internal links to include baseurl (for GitHub Pages, etc.)
        # This handles standard markdown links like [text](/path/) by prepending baseurl
        parsed_content = self._transform_internal_links(parsed_content)

        page.parsed_ast = parsed_content

        # Post-process: Enhance API documentation with badges
        # (inject HTML badges for @async, @property, etc. markers)
        # Prefer injected enhancer if present in BuildContext, else use singleton
        try:
            enhancer = None
            if self.build_context and getattr(self.build_context, "api_doc_enhancer", None):
                enhancer = self.build_context.api_doc_enhancer
            if enhancer is None:
                from bengal.rendering.api_doc_enhancer import get_enhancer

                enhancer = get_enhancer()
        except Exception:
            enhancer = None
        page_type = page.metadata.get("type")
        if enhancer and enhancer.should_enhance(page_type):
            before_enhancement = page.parsed_ast
            page.parsed_ast = enhancer.enhance(page.parsed_ast, page_type)

            # Debug output only in dev mode
            from bengal.utils.profile import should_show_debug

            if (
                should_show_debug()
                and "@property" in before_enhancement
                and "page.md" in str(page.source_path)
                and "core" in str(page.source_path)
            ):
                logger.debug(
                    "api_doc_enhancement",
                    source_path=str(page.source_path),
                    before_chars=len(before_enhancement),
                    after_chars=len(page.parsed_ast),
                    has_markers=("@property" in before_enhancement),
                    has_badges=("api-badge" in page.parsed_ast),
                )

        # ============================================================================
        # Build Phase: PARSING COMPLETE
        # ============================================================================
        # At this point:
        # - page.parsed_ast contains HTML (post-markdown, pre-template)
        # - page.toc contains TOC HTML
        # - page.toc_items property can now extract structured TOC data
        #
        # Note: toc_items is a lazy @property that:
        # - Returns [] if accessed before this point (doesn't cache empty)
        # - Extracts and caches structure when accessed after this point
        # ============================================================================
        page.toc = toc

        # OPTIMIZATION #2: Store parsed content in cache for next build
        if self.dependency_tracker and hasattr(self.dependency_tracker, "_cache"):
            cache = self.dependency_tracker._cache
            if cache and not page.metadata.get("_generated"):
                # Extract TOC items for caching
                toc_items = extract_toc_structure(toc)

                cache.store_parsed_content(
                    page.source_path,
                    parsed_content,
                    toc,
                    toc_items,
                    page.metadata,
                    template,
                    parser_version,
                )

        # Stage 3: Extract links for validation
        page.extract_links()

        # Stage 4: Render content to HTML
        html_content = self.renderer.render_content(parsed_content)

        # Stage 5: Apply template (with dependency tracking already set in __init__)
        page.rendered_html = self.renderer.render_page(page, html_content)

        # Stage 6: HTML formatting (pristine output)
        try:
            from bengal.postprocess.html_output import format_html_output

            # Resolve mode from config with backward compatibility
            # Priority: page.metadata.no_format → html_output.mode → minify_html
            if page.metadata.get("no_format") is True:
                mode = "raw"
                options = {}
            else:
                html_cfg = self.site.config.get("html_output", {}) or {}
                mode = html_cfg.get(
                    "mode",
                    "minify" if self.site.config.get("minify_html", True) else "pretty",
                )
                options = {
                    "remove_comments": html_cfg.get("remove_comments", mode == "minify"),
                    "collapse_blank_lines": html_cfg.get("collapse_blank_lines", True),
                }
            page.rendered_html = format_html_output(page.rendered_html, mode=mode, options=options)
        except Exception:
            # Never fail the build on formatter errors; fall back to raw HTML
            pass

        # Stage 7: Write output
        self._write_output(page)

        # End page tracking
        if self.dependency_tracker and not page.metadata.get("_generated"):
            self.dependency_tracker.end_page()

    def _escape_template_syntax_in_html(self, html: str) -> str:
        """
        Escape Jinja2 variable delimiters in already-rendered HTML.

        Converts "{{" and "}}" to HTML entities so they appear literally
        in documentation pages but won't be detected by tests as unrendered.
        """
        try:
            return html.replace("{{", "&#123;&#123;").replace("}}", "&#125;&#125;")
        except Exception:
            return html

    def _escape_jinja_blocks(self, html: str) -> str:
        """
        Escape Jinja2 block delimiters in already-rendered HTML content.

        Converts "{%" and "%}" to HTML entities to avoid leaking raw
        control-flow markers into final HTML outside template processing.
        """
        try:
            return html.replace("{%", "&#123;%").replace("%}", "%&#125;")
        except Exception:
            return html

    def _transform_internal_links(self, html: str) -> str:
        """
        Transform internal links to include baseurl prefix.

        This handles standard markdown links like [text](/path/) by prepending
        the configured baseurl. Essential for GitHub Pages project sites and
        similar deployments where the site is not at the domain root.

        Args:
            html: Rendered HTML content

        Returns:
            HTML with transformed internal links
        """
        try:
            from bengal.rendering.link_transformer import (
                get_baseurl,
                should_transform_links,
                transform_internal_links,
            )

            if not should_transform_links(self.site.config):
                return html

            baseurl = get_baseurl(self.site.config)
            return transform_internal_links(html, baseurl)
        except Exception as e:
            # Never fail the build on link transformation errors
            logger.debug("link_transformation_error", error=str(e))
            return html

    def _write_output(self, page: Page) -> None:
        """
        Write rendered page to output directory.

        Args:
            page: Page with rendered content
        """
        # Ensure parent directory exists (with caching to reduce syscalls)
        parent_dir = page.output_path.parent

        # Only create directory if not already done (thread-safe check)
        if parent_dir not in _created_dirs:
            with _created_dirs_lock:
                # Double-check inside lock to avoid race condition
                if parent_dir not in _created_dirs:
                    parent_dir.mkdir(parents=True, exist_ok=True)
                    _created_dirs.add(parent_dir)

        # Write rendered HTML (atomic for safety, fast mode for performance)
        # Fast mode skips atomic writes for dev server (PERFORMANCE OPTIMIZATION)
        fast_writes = self.site.config.get("build", {}).get("fast_writes", False)

        if fast_writes:
            # Direct write (faster, but not crash-safe)
            page.output_path.write_text(page.rendered_html, encoding="utf-8")
        else:
            # Atomic write (crash-safe, slightly slower)
            from bengal.utils.atomic_write import atomic_write_text

            atomic_write_text(page.output_path, page.rendered_html, encoding="utf-8")

        # Track source→output mapping for cleanup on deletion
        # (Skip generated pages - they have virtual paths)
        if (
            self.dependency_tracker
            and not page.metadata.get("_generated")
            and hasattr(self.dependency_tracker, "cache")
            and self.dependency_tracker.cache
        ):
            self.dependency_tracker.cache.track_output(
                page.source_path, page.output_path, self.site.output_dir
            )

        # Only print in verbose mode
        if not self.quiet:
            msg = f"  ✓ {page.output_path.relative_to(self.site.output_dir)}"
            reporter = getattr(self, "build_context", None) and getattr(
                self.build_context, "reporter", None
            )
            if reporter:
                try:
                    reporter.log(msg)
                except Exception:
                    print(msg)
            else:
                print(msg)

    def _determine_output_path(self, page: Page) -> Path:
        """
        Determine the output path for a page.

        Args:
            page: Page to determine path for

        Returns:
            Output path
        """
        # Delegate path computation to centralized URLStrategy (i18n-aware)
        return URLStrategy.compute_regular_page_output_path(page, self.site)

    def _determine_template(self, page: Page) -> str:
        """
        Determine which template will be used for this page.

        Args:
            page: Page object

        Returns:
            Template name (e.g., 'single.html', 'page.html')
        """
        # Check page-specific template first
        if hasattr(page, "template") and page.template:
            return page.template

        # Check metadata
        if "template" in page.metadata:
            return page.metadata["template"]

        # Default based on page type
        page_type = page.metadata.get("type", "page")

        match page_type:
            case "page":
                return "page.html"
            case "section":
                return "list.html"
            case _ if page.metadata.get("is_section"):
                return "list.html"
            case _:
                return "single.html"

    def _get_parser_version(self) -> str:
        """
        Get parser version string for cache validation.

        Includes both parser library version and TOC extraction version to
        invalidate cache when TOC parsing logic changes.

        Returns:
            Parser version (e.g., "mistune-3.0-toc2", "markdown-3.4-toc2")
        """
        parser_name = type(self.parser).__name__

        # Try to get actual version
        match parser_name:
            case "MistuneParser":
                try:
                    import mistune

                    base_version = f"mistune-{mistune.__version__}"
                except (ImportError, AttributeError):
                    base_version = "mistune-unknown"
            case "PythonMarkdownParser":
                try:
                    import markdown

                    base_version = f"markdown-{markdown.__version__}"
                except (ImportError, AttributeError):
                    base_version = "markdown-unknown"
            case _:
                base_version = f"{parser_name}-unknown"

        # Add TOC extraction version to invalidate cache when extraction logic changes
        return f"{base_version}-toc{TOC_EXTRACTION_VERSION}"

    def _preprocess_content(self, page: Page) -> str:
        """
        Pre-process page content through Jinja2 to allow variable substitution.

        This allows technical writers to use {{ page.metadata.xxx }} directly
        in their markdown content, not just in templates.

        Pages can disable preprocessing by setting `preprocess: false` in frontmatter.
        This is useful for documentation pages that show Jinja2 syntax examples.

        Args:
            page: Page to pre-process

        Returns:
            Content with Jinja2 variables rendered

        Example:
            # In markdown:
            Today we're talking about {{ page.metadata.product_name }}
            version {{ page.metadata.version }}.
        """
        # Skip preprocessing if disabled in frontmatter
        if page.metadata.get("preprocess") is False:
            return page.content

        from jinja2 import Template, TemplateSyntaxError

        try:
            # Create a Jinja2 template from the content
            template = Template(page.content)

            # Render with page and site context
            rendered_content = template.render(page=page, site=self.site, config=self.site.config)

            return rendered_content

        except TemplateSyntaxError as e:
            # If there's a syntax error, warn but continue with original content
            if self.build_stats:
                self.build_stats.add_warning(str(page.source_path), str(e), "jinja2")
            else:
                logger.warning(
                    "jinja2_syntax_error",
                    source_path=str(page.source_path),
                    error=truncate_error(e),
                    error_type=type(e).__name__,
                )
            if not self.quiet and not self.build_stats:
                print(f"  ⚠️  Jinja2 syntax error in {page.source_path}: {truncate_error(e)}")
            return page.content
        except Exception as e:
            # For any other error, warn but continue
            if self.build_stats:
                self.build_stats.add_warning(
                    str(page.source_path), truncate_error(e), "preprocessing"
                )
            else:
                logger.warning(
                    "preprocessing_error",
                    source_path=str(page.source_path),
                    error=truncate_error(e),
                    error_type=type(e).__name__,
                )
            if not self.quiet and not self.build_stats:
                print(f"  ⚠️  Error pre-processing {page.source_path}: {truncate_error(e)}")
            return page.content


# TOC extraction version - increment when extract_toc_structure() logic changes
TOC_EXTRACTION_VERSION = "2"  # v2: Added regex-based indentation parsing for mistune


def extract_toc_structure(toc_html: str) -> list:
    """
    Parse TOC HTML into structured data for custom rendering.

    Handles both nested <ul> structures (python-markdown style) and flat lists (mistune style).
    For flat lists from mistune, parses indentation to infer heading levels.

    This is a standalone function so it can be called from Page.toc_items
    property for lazy evaluation.

    Args:
        toc_html: HTML table of contents

    Returns:
        List of TOC items with id, title, and level (1=H2, 2=H3, 3=H4, etc.)
    """
    if not toc_html:
        return []

    import re

    try:
        # For mistune's flat TOC with indentation, use regex to preserve whitespace
        # Pattern: optional spaces + <li><a href="#id">title</a></li>
        pattern = r'^(\s*)<li><a href="#([^"]+)">([^<]+)</a></li>'

        items = []
        for line in toc_html.split("\n"):
            match = re.match(pattern, line)
            if match:
                indent_str, anchor_id, title = match.groups()
                # Decode HTML entities (e.g., &quot; -> ", &amp; -> &)
                title = html_module.unescape(title)
                # Count spaces to determine level (mistune uses 2 spaces per level)
                indent_level = len(indent_str)
                level = (
                    indent_level // 2
                ) + 1  # 0 spaces = level 1 (H2), 2 spaces = level 2 (H3), etc.

                items.append({"id": anchor_id, "title": title, "level": level})

        if items:
            return items

        # Fallback to HTML parser for nested structures (python-markdown style)
        from html.parser import HTMLParser

        class TOCParser(HTMLParser):
            def __init__(self):
                super().__init__()
                self.items = []
                self.current_item = None
                self.depth = 0

            def handle_starttag(self, tag, attrs):
                if tag == "ul":
                    self.depth += 1
                elif tag == "a":
                    attrs_dict = dict(attrs)
                    self.current_item = {
                        "id": attrs_dict.get("href", "").lstrip("#"),
                        "title": "",
                        "level": self.depth,
                    }

            def handle_data(self, data):
                if self.current_item is not None:
                    # Decode HTML entities (e.g., &quot; -> ", &amp; -> &)
                    decoded_data = html_module.unescape(data.strip())
                    self.current_item["title"] += decoded_data

            def handle_endtag(self, tag):
                if tag == "ul":
                    self.depth -= 1
                elif tag == "a" and self.current_item:
                    if self.current_item["title"]:
                        self.items.append(self.current_item)
                    self.current_item = None

        parser = TOCParser()
        parser.feed(toc_html)
        return parser.items

    except Exception:
        # If parsing fails, return empty list
        return []
