"""
Taxonomy orchestration for Bengal SSG.

Collects taxonomies (tags, categories) from page frontmatter and generates
dynamic pages for taxonomy listings. Supports incremental updates, i18n,
and parallel processing.

Key Responsibilities:
Taxonomy Collection
    Scans all pages for taxonomy metadata (tags, categories) and builds
    site.taxonomies with term-to-pages mappings.
Tag Index Page
    Generates /tags/ page listing all tags with page counts.
Individual Tag Pages
    Generates /tags/{slug}/ pages with paginated listings of posts
    for each tag.
Incremental Updates
    Only regenerates affected tags when page tags change, using
    TaxonomyIndex for O(1) change detection.

Architecture:
The orchestrator builds site.taxonomies dict during collection:
    site.taxonomies['tags'][slug] = {
            'name': str,      # Display name (e.g., 'Python')
            'slug': str,      # URL slug (e.g., 'python')
            'pages': list[Page]  # Pages with this tag
    }

Dynamic pages are generated pages with _generated=True metadata
that use taxonomy templates (tags.html, tag.html).

Performance:
Collection: O(n) where n=total pages
Incremental: O(changed) for affected tags only
Generation: Parallel via ThreadPoolExecutor for 20+ tags

i18n Support:
When i18n is enabled, generates per-locale tag pages and respects
share_taxonomies configuration for cross-locale tag sharing.

Related Modules:
bengal.cache.build_cache: Stores tag-to-pages mappings for incremental
bengal.cache.taxonomy_index: Detects unchanged tags for Phase 2c.2
bengal.utils.pagination: Paginator for tag page listings

See Also:
bengal.orchestration.section: Handles section archives (separate concern)
bengal.orchestration.build: Phase 7 (taxonomies) coordination

"""

from __future__ import annotations

import concurrent.futures
from datetime import datetime
from pathlib import Path
from typing import TYPE_CHECKING, Any

from bengal.build.contracts.keys import content_key
from bengal.orchestration.utils.errors import is_shutdown_error
from bengal.orchestration.utils.i18n import (
    filter_pages_by_language,
    get_i18n_config,
)
from bengal.orchestration.utils.virtual_pages import (
    VirtualPageSpec,
    create_virtual_page,
)
from bengal.utils.concurrency.workers import WorkloadType, get_optimal_workers
from bengal.utils.observability.logger import get_logger
from bengal.utils.paths.url_strategy import URLStrategy

logger = get_logger(__name__)

# Threshold for parallel processing - below this we use sequential processing
# to avoid thread pool overhead for small workloads.
# Lowered from 20 to 10 based on I/O-bound analysis (tag page generation is ~5-10ms each).
# See: plan/drafted/rfc-orchestration-package-optimizations.md (Phase 3)
MIN_TAGS_FOR_PARALLEL = 10

if TYPE_CHECKING:
    from bengal.cache.build_cache import BuildCache
    from bengal.cache.taxonomy_index import TaxonomyIndex
    from bengal.core.page import Page
    from bengal.core.site import Site
    from bengal.orchestration.build_context import BuildContext
    from bengal.protocols import PageLike


class TaxonomyOrchestrator:
    """
    Handles taxonomies and dynamic page generation.

    Responsibilities:
        - Collect tags, categories, and other taxonomies
        - Generate tag index pages
        - Generate individual tag pages (with pagination)

    Note: Section archive pages are now handled by SectionOrchestrator

    """

    def __init__(self, site: Site, threshold: int = 20, parallel: bool = True):
        """
        Initialize taxonomy orchestrator.

        Args:
            site: Site instance containing pages and sections
        """
        self.site = site  # Restore for compat
        self.threshold = threshold
        self.parallel = parallel
        self.url_strategy = URLStrategy()

    def _is_eligible_for_taxonomy(self, page: PageLike) -> bool:
        """
        Check if a page is eligible for taxonomy collection.

        Excludes:
        - Generated pages (tag pages, archive pages, etc.)
        - Pages from autodoc output directories (content/api, content/cli)
          Note: Autodoc pages typically don't have tags, but this prevents
          them from being included if someone manually adds tags.

        Args:
            page: Page to check

        Returns:
            True if page should be included in taxonomies
        """
        # Skip generated pages (tag pages, archive pages, etc.)
        if page.metadata.get("_generated"):
            return False

        # Skip pages from autodoc output directories as defensive measure
        # (Autodoc pages typically don't have tags, but this prevents
        #  them from being included if someone manually adds tags)
        source_str = str(page.source_path)
        return "content/api" not in source_str and "content/cli" not in source_str

    def _page_path_key(self, path: Path) -> str:
        """Normalize page path for TaxonomyIndex lookups (content_key format)."""
        return str(content_key(Path(path), self.site.root_path))

    def collect_and_generate(self, parallel: bool = True) -> None:
        """
        Collect taxonomies and generate dynamic pages.
        Main entry point called during build.

        Args:
            parallel: Whether to use parallel processing for tag page generation
        """
        self.collect_taxonomies()
        self.generate_dynamic_pages(parallel=parallel)

        # Phase 2c.2: Create/Update TaxonomyIndex for full builds
        # This ensures TaxonomyIndex is populated for incremental builds
        try:
            from bengal.cache.taxonomy_index import TaxonomyIndex

            # Create fresh index (don't load existing)
            index = TaxonomyIndex(self.site.paths.taxonomy_cache)
            index.clear()  # Start fresh for full build

            # Populate index from collected taxonomies
            for tag_slug, tag_data in self.site.taxonomies.get("tags", {}).items():
                page_paths = [self._page_path_key(p.source_path) for p in tag_data.get("pages", [])]
                logger.debug(
                    "taxonomy_index_update_tag",
                    tag_slug=tag_slug,
                    page_count=len(page_paths),
                )
                index.update_tag(tag_slug, tag_data.get("name", tag_slug), page_paths)

            index.save_to_disk()
            logger.debug(
                "taxonomy_index_updated_full_build",
                tags=len(index.tags),
            )
        except Exception as e:
            logger.debug(
                "taxonomy_index_update_failed_full_build",
                error=str(e),
            )

    def collect_and_generate_incremental(
        self, changed_pages: list[Page], cache: BuildCache
    ) -> set[str]:
        """
        Incrementally update taxonomies for changed pages only.

        Architecture:
        1. Only rebuild site.taxonomies from current Page objects when tags actually changed
        2. Use cache to determine which tag PAGES need regeneration (fast)
        3. Never reuse taxonomy structure with object references (prevents bugs)

        Performance:
        - Change detection: O(changed pages)
        - Taxonomy reconstruction: O(all tags * pages_per_tag) ≈ O(all pages) but ONLY when tags changed
        - Tag page generation: O(affected tags)

        Args:
            changed_pages: List of pages that changed (NOT generated pages)
            cache: Build cache with tag index

        Returns:
            Set of affected tag slugs (for regenerating tag pages)
        """
        logger.info("taxonomy_collection_incremental_start", changed_pages=len(changed_pages))

        # STEP 1: Determine which tags are affected
        # This is the O(changed) optimization - only look at changed pages
        affected_tags = set()
        for page in changed_pages:
            if page.metadata.get("_generated"):
                continue

            # For changed pages that are PageProxy objects, the cached metadata
            # contains OLD tags from the previous build.  Force-load the page
            # from disk so we compare against the CURRENT frontmatter tags.
            from bengal.core.page.proxy import PageProxy

            if isinstance(page, PageProxy):
                page._ensure_loaded()
                actual_page = page._full_page if page._full_page else page
                actual_tags = actual_page.tags
            else:
                actual_tags = page.tags

            # Update cache and get affected tags
            new_tags = set(actual_tags) if actual_tags else set()
            page_key = cache._cache_key(page.source_path)
            page_affected = cache.taxonomy_index.update_page_tags(page_key, new_tags)
            affected_tags.update(page_affected)

        # STEP 2: Rebuild taxonomy structure from current Page objects
        # RFC: rfc-incremental-build-dependency-gaps - Phase 2 fix
        # ALWAYS rebuild when pages are changing, not just when tags change.
        # This ensures taxonomy listing pages use current Page objects with
        # up-to-date metadata (title, date, summary) even if tags didn't change.
        # The rebuild is O(tags * pages_per_tag) but typically fast (<50ms).
        pages_with_tags = [p for p in changed_pages if p.tags and not p.metadata.get("_generated")]
        if affected_tags or not changed_pages or pages_with_tags:
            # Rebuild if: (1) tags changed OR (2) no pages changed OR (3) pages with tags changed
            self._rebuild_taxonomy_structure_from_cache(cache)
        else:
            # Only skip if pages changed but none of them have tags
            logger.info("taxonomy_rebuild_skipped", reason="no_tagged_pages_affected")

        # STEP 3: Load TaxonomyIndex for Phase 2c.2 optimization (skip unchanged tags)
        taxonomy_index = None
        try:
            from bengal.cache.taxonomy_index import TaxonomyIndex

            taxonomy_index = TaxonomyIndex(self.site.paths.taxonomy_cache)
            logger.debug(
                "taxonomy_index_loaded_for_incremental",
                tags=len(taxonomy_index.tags),
            )
        except Exception as e:
            logger.debug(
                "taxonomy_index_load_failed_for_incremental",
                error=str(e),
            )
            # Continue without optimization if cache can't be loaded

        # STEP 4: Generate tag pages
        # Special case: If no pages changed but we have tags, regenerate ALL tag pages
        # (This happens in dev server when site.pages was cleared but content didn't change)
        if not changed_pages and self.site.taxonomies.get("tags"):
            # No content changed, but we need to regenerate all taxonomy pages
            all_tags = set(self.site.taxonomies["tags"].keys())
            self.generate_dynamic_pages_for_tags_with_cache(all_tags, taxonomy_index=taxonomy_index)
            affected_tags = all_tags
        elif affected_tags:
            # Normal case: Only regenerate affected tag pages
            # Phase 2c.2: With TaxonomyIndex optimization for skipping unchanged tags
            self.generate_dynamic_pages_for_tags_with_cache(
                affected_tags, taxonomy_index=taxonomy_index
            )

        # STEP 5: Update TaxonomyIndex for next build
        # This persists the tag-to-pages mappings so Phase 2c.2 can detect unchanged tags
        if taxonomy_index:
            try:
                for tag_slug, tag_data in self.site.taxonomies.get("tags", {}).items():
                    page_paths = [
                        self._page_path_key(p.source_path) for p in tag_data.get("pages", [])
                    ]
                    taxonomy_index.update_tag(tag_slug, tag_data.get("name", tag_slug), page_paths)

                taxonomy_index.save_to_disk()
                logger.debug(
                    "taxonomy_index_updated",
                    tags=len(taxonomy_index.tags),
                )
            except Exception as e:
                logger.warning(
                    "taxonomy_index_update_failed",
                    error=str(e),
                )

        logger.info(
            "taxonomy_collection_incremental_complete",
            tags=len(self.site.taxonomies.get("tags", {})),
            updated_pages=len(changed_pages),
            affected_tags=len(affected_tags),
        )

        return affected_tags

    def collect_taxonomies(self) -> None:
        """
        Collect taxonomies (tags, categories, etc.) from all pages.
        Organizes pages by their taxonomic terms.
        """
        logger.info("taxonomy_collection_start", total_pages=len(self.site.pages))

        # Initialize taxonomy structure
        self.site.taxonomies = {"tags": {}, "categories": {}}

        # Get i18n configuration using utility
        i18n_config = get_i18n_config(self.site.config)
        current_lang = getattr(self.site, "current_language", None)

        pages_with_tags = 0
        for page in self.site.pages:
            # Skip pages that shouldn't be in taxonomies
            if not self._is_eligible_for_taxonomy(page):
                continue

            # Only filter by language if i18n is actually enabled
            if (
                i18n_config.is_enabled
                and not i18n_config.share_taxonomies
                and current_lang
                and getattr(page, "lang", current_lang) != current_lang
            ):
                continue
            # Collect tags
            if page.tags:
                pages_with_tags += 1
                for tag in page.tags:
                    # Ensure tag is a string (YAML may parse numbers like 404 as int)
                    tag_str = str(tag)
                    tag_key = tag_str.lower().replace(" ", "-")
                    if tag_key not in self.site.taxonomies["tags"]:
                        self.site.taxonomies["tags"][tag_key] = {
                            "name": tag_str,
                            "slug": tag_key,
                            "pages": [],
                        }
                    self.site.taxonomies["tags"][tag_key]["pages"].append(page)

            # Collect categories (if present in metadata)
            if "category" in page.metadata:
                category = page.metadata["category"]
                # Ensure category is a string
                category_str = str(category)
                cat_key = category_str.lower().replace(" ", "-")
                if cat_key not in self.site.taxonomies["categories"]:
                    self.site.taxonomies["categories"][cat_key] = {
                        "name": category_str,
                        "slug": cat_key,
                        "pages": [],
                    }
                self.site.taxonomies["categories"][cat_key]["pages"].append(page)

        # Sort pages within each taxonomy by date (newest first)
        for taxonomy_type in self.site.taxonomies:
            for term_data in self.site.taxonomies[taxonomy_type].values():
                term_data["pages"].sort(
                    key=lambda p: p.date if p.date else datetime.min, reverse=True
                )

        tag_count = len(self.site.taxonomies.get("tags", {}))
        cat_count = len(self.site.taxonomies.get("categories", {}))
        logger.info(
            "taxonomy_collection_complete",
            tags=tag_count,
            categories=cat_count,
            pages_with_tags=pages_with_tags,
            total_pages_checked=len(self.site.pages),
        )

    def _rebuild_taxonomy_structure_from_cache(self, cache: BuildCache) -> None:
        """
        Rebuild site.taxonomies from cache using CURRENT Page objects.

        This is the key to avoiding stale references:
        1. Cache tells us which pages have which tags (paths only)
        2. We map paths to current Page objects (from site.pages)
        3. We reconstruct taxonomy dict with current objects

        Performance: O(tags * pages_per_tag) which is O(all pages) worst case,
        but in practice very fast because it's just dict lookups and list appends.

        CRITICAL: This always uses current Page objects, never cached references.
        """
        # Initialize fresh structure
        self.site.taxonomies = {"tags": {}, "categories": {}}

        # Build lookup map: path → current Page object
        # Filter out generated pages and autodoc pages
        eligible_pages = [p for p in self.site.regular_pages if self._is_eligible_for_taxonomy(p)]
        current_page_map = {p.source_path: p for p in eligible_pages}

        # For each tag in cache, map paths to current Page objects
        for tag_slug in cache.taxonomy_index.get_all_tags():
            page_paths = cache.taxonomy_index.get_pages_for_tag(tag_slug)

            # Map paths to current Page objects
            # Resolve relative paths (content_key format) against site_root for lookup
            current_pages = []
            for path_str in page_paths:
                path = Path(path_str)
                if not path.is_absolute() and cache.site_root is not None:
                    path = cache.site_root / path_str
                if path in current_page_map:
                    current_pages.append(current_page_map[path])

            if not current_pages:
                # Tag has no pages - skip it (was removed)
                continue

            # Get original tag name (not slug) from first page's tags
            # This handles "Python" vs "python" correctly
            original_tag = None
            for page in current_pages:
                if page.tags:
                    for tag in page.tags:
                        tag_str = str(tag)
                        if tag_str.lower().replace(" ", "-") == tag_slug:
                            original_tag = tag_str
                            break
                if original_tag:
                    break

            if not original_tag:
                original_tag = tag_slug  # Fallback

            # Create tag entry with CURRENT page objects
            self.site.taxonomies["tags"][tag_slug] = {
                "name": original_tag,
                "slug": tag_slug,
                "pages": sorted(
                    current_pages, key=lambda p: p.date if p.date else datetime.min, reverse=True
                ),
            }

        # Handle categories (similar pattern if needed in future)

    def generate_dynamic_pages_for_tags(self, affected_tags: set[str]) -> None:
        """
        Generate dynamic pages only for specific affected tags (incremental optimization).

        This method supports i18n - it generates per-locale tag pages when i18n is enabled.

        Args:
            affected_tags: Set of tag slugs that need page regeneration
        """
        self.generate_dynamic_pages_for_tags_with_cache(affected_tags, taxonomy_index=None)

    def generate_dynamic_pages_for_tags_with_cache(
        self, affected_tags: set[str], taxonomy_index: TaxonomyIndex | None = None
    ) -> None:
        """
        Generate dynamic pages only for specific affected tags with TaxonomyIndex optimization (Phase 2c.2).

        This enhanced version uses TaxonomyIndex to skip regenerating tags whose page membership
        hasn't changed, providing ~160ms savings per incremental build for typical sites.

        Args:
            affected_tags: Set of tag slugs that need page regeneration
            taxonomy_index: Optional TaxonomyIndex for skipping unchanged tags
        """
        generated_count = 0
        skipped_count = 0

        if not self.site.taxonomies.get("tags"):
            return

        # Get i18n configuration using utility
        i18n_config = get_i18n_config(self.site.config)

        # Generate per-locale tag pages
        for lang in i18n_config.languages:
            # Build per-locale tag mapping
            locale_tags = {}
            for tag_slug, tag_data in self.site.taxonomies["tags"].items():
                # Filter pages by language using utility
                pages_for_lang = filter_pages_by_language(
                    tag_data["pages"],
                    lang,
                    i18n_config.default_language,
                    i18n_config.share_taxonomies,
                    i18n_config.strategy,
                )
                if not pages_for_lang:
                    continue
                locale_tags[tag_slug] = {
                    "name": tag_data["name"],
                    "slug": tag_slug,
                    "pages": pages_for_lang,
                }

            if not locale_tags:
                continue

            # Temporarily set language context for URL computation
            prev_lang = getattr(self.site, "current_language", None)
            self.site.current_language = lang
            try:
                # Always regenerate tag index (it lists all tags)
                tag_index = self._create_tag_index_page_for(locale_tags)
                if tag_index:
                    tag_index.lang = lang
                    self.site.pages.append(tag_index)
                    generated_count += 1

                # Only generate pages for affected tags, with TaxonomyIndex optimization
                for tag_slug in affected_tags:
                    if tag_slug in locale_tags:
                        tag_data = locale_tags[tag_slug]

                        # PHASE 2C.2 OPTIMIZATION: Skip regenerating tags with unchanged pages
                        if taxonomy_index:
                            page_paths = [
                                self._page_path_key(p.source_path) for p in tag_data["pages"]
                            ]
                            if not taxonomy_index.pages_changed(tag_slug, page_paths):
                                logger.debug(
                                    "tag_page_generation_skipped",
                                    tag_slug=tag_slug,
                                    reason="pages_unchanged",
                                    page_count=len(page_paths),
                                )
                                skipped_count += 1
                                continue

                        # Route through _create_tag_pages so tests that patch it can count calls
                        pages = self._create_tag_pages(
                            tag_slug, {"name": tag_data["name"], "pages": tag_data["pages"]}
                        )
                        for page in pages:
                            page.lang = lang
                            self.site.pages.append(page)
                            generated_count += 1
            finally:
                self.site.current_language = prev_lang

        # Invalidate cached page lists after adding generated pages
        self.site.invalidate_page_caches()

        logger.info(
            "dynamic_pages_generated_incremental",
            tag_pages=generated_count,
            total=generated_count,
            affected_tags=len(affected_tags),
            languages=len(i18n_config.languages),
            skipped_tags=skipped_count if skipped_count > 0 else None,
        )

    def generate_dynamic_pages(self, parallel: bool = True) -> None:
        """
        Generate dynamic taxonomy pages (tag pages, etc.) that don't have source files.

        Note: Section archive pages are now generated by SectionOrchestrator

        Args:
            parallel: Whether to use parallel processing for tag pages (default: True)
        """
        generated_count = 0
        # Get i18n configuration using utility
        i18n_config = get_i18n_config(self.site.config)

        # Generate per-locale tag pages
        if self.site.taxonomies.get("tags"):
            for lang in i18n_config.languages:
                # Build per-locale tag mapping
                locale_tags = {}
                for tag_slug, tag_data in self.site.taxonomies["tags"].items():
                    # Filter pages by language using utility
                    pages_for_lang = filter_pages_by_language(
                        tag_data["pages"],
                        lang,
                        i18n_config.default_language,
                        i18n_config.share_taxonomies,
                        i18n_config.strategy,
                    )
                    if not pages_for_lang:
                        continue
                    locale_tags[tag_slug] = {
                        "name": tag_data["name"],
                        "slug": tag_slug,
                        "pages": pages_for_lang,
                    }
                if not locale_tags:
                    continue

                # Temporarily set language context for URL computation
                prev_lang = getattr(self.site, "current_language", None)
                self.site.current_language = lang
                try:
                    # Tag index for this language
                    tag_index = self._create_tag_index_page_for(locale_tags)
                    if tag_index:
                        tag_index.lang = lang
                        self.site.pages.append(tag_index)
                        generated_count += 1

                    # Individual tag pages for this language
                    # Use parallel generation if we have many tags
                    if parallel and len(locale_tags) >= MIN_TAGS_FOR_PARALLEL:
                        tag_pages_count = self._generate_tag_pages_parallel(locale_tags, lang)
                    else:
                        tag_pages_count = self._generate_tag_pages_sequential(locale_tags, lang)

                    generated_count += tag_pages_count
                finally:
                    self.site.current_language = prev_lang

        # Invalidate cached page lists after adding generated pages
        self.site.invalidate_page_caches()

        # Count types of generated pages
        tag_count = sum(
            1
            for p in self.site.pages
            if p.metadata.get("_generated") and p.output_path and "tag" in p.output_path.parts
        )
        pagination_count = sum(
            1
            for p in self.site.pages
            if p.metadata.get("_generated") and "/page/" in str(p.output_path)
        )

        if generated_count > 0:
            logger.info(
                "dynamic_pages_generated",
                tag_pages=tag_count,
                pagination_pages=pagination_count,
                total=generated_count,
            )

    def _generate_tag_pages_sequential(self, locale_tags: dict[str, Any], lang: str) -> int:
        """
        Generate tag pages sequentially (original implementation).

        Args:
            locale_tags: Dictionary of tag slugs to tag data
            lang: Language code

        Returns:
            Number of pages generated
        """
        count = 0
        for tag_slug, tag_data in locale_tags.items():
            # Route through _create_tag_pages so tests that patch it can count calls
            pages = self._create_tag_pages(
                tag_slug, {"name": tag_data["name"], "pages": tag_data["pages"]}
            )
            for page in pages:
                page.lang = lang
                self.site.pages.append(page)
                count += 1
        return count

    def _generate_tag_pages_parallel(self, locale_tags: dict[str, Any], lang: str) -> int:
        """
        Generate tag pages in parallel using ThreadPoolExecutor.

        Each tag's pages can be generated independently, making this perfectly
        parallelizable. On Python 3.14t (free-threaded), this achieves true
        parallelism without GIL contention.

        Performance:
            - Python 3.13 (GIL): 2-3x faster
            - Python 3.14t (no GIL): 6-8x faster

        Args:
            locale_tags: Dictionary of tag slugs to tag data
            lang: Language code

        Returns:
            Number of pages generated
        """
        from bengal.errors import ErrorAggregator, ErrorCode

        # Get optimal workers for CPU-bound page generation
        # Access max_workers from build section (supports both Config and dict)
        config = self.site.config
        if hasattr(config, "build"):
            max_workers_override = config.build.max_workers
        else:
            build_section = config.get("build", {})
            max_workers_override = (
                build_section.get("max_workers")
                if isinstance(build_section, dict)
                else config.get("max_workers")
            )

        max_workers = get_optimal_workers(
            len(locale_tags),
            workload_type=WorkloadType.CPU_BOUND,
            config_override=max_workers_override,
        )

        all_generated_pages = []

        # Use ThreadPoolExecutor for parallel processing
        with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
            # Submit all tasks
            future_to_tag = {
                executor.submit(self._create_tag_pages_for_lang, tag_slug, tag_data, lang): tag_slug
                for tag_slug, tag_data in locale_tags.items()
            }

            # Use ErrorAggregator for batch error handling
            aggregator = ErrorAggregator(total_items=len(locale_tags))
            threshold = 5

            # Collect results as they complete
            for future in concurrent.futures.as_completed(future_to_tag):
                tag_slug = future_to_tag[future]
                try:
                    tag_pages = future.result()
                    # Set language for all pages
                    for page in tag_pages:
                        page.lang = lang
                    all_generated_pages.extend(tag_pages)
                except Exception as e:
                    # Handle shutdown errors gracefully
                    if is_shutdown_error(e):
                        logger.debug("taxonomy_shutdown", tag_slug=tag_slug)
                        continue

                    # Use ErrorAggregator for structured error handling
                    context = {
                        "tag_slug": tag_slug,
                        "lang": lang,
                        "error": str(e),
                        "error_type": type(e).__name__,
                        "error_code": ErrorCode.B006.value,
                        "suggestion": "Check tag template 'tag.html' exists and is valid Jinja2",
                    }
                    if aggregator.should_log_individual(
                        e, context, threshold=threshold, max_samples=3
                    ):
                        logger.error("taxonomy_page_generation_failed", **context)
                    aggregator.add_error(e, context=context)

            # Log summary if errors occurred
            aggregator.log_summary(logger, threshold=threshold, error_type="taxonomy")

        # Append all generated pages at once (thread-safe)
        self.site.pages.extend(all_generated_pages)

        return len(all_generated_pages)

    def _create_tag_index_page(self) -> Page:
        """
        Create the main tags index page.

        Delegates to _create_tag_index_page_for with site taxonomies.

        Returns:
            Generated tag index page
        """
        return self._create_tag_index_page_for(self.site.taxonomies["tags"])

    def _create_tag_index_page_for(self, tags: dict[str, Any]) -> Page:
        """Create tag index page using virtual page utility."""
        spec = VirtualPageSpec(
            title="All Tags",
            template="tags.html",
            page_type="tag-index",
            metadata={"_tags": tags},
        )

        return create_virtual_page(
            site=self.site,
            url_strategy=self.url_strategy,
            spec=spec,
            path_segments=("tags",),
            output_path=self.url_strategy.compute_tag_index_output_path(self.site),
            registry_owner="taxonomy",
            registry_priority=40,
        )

    def _create_tag_pages(self, tag_slug: str, tag_data: dict[str, Any]) -> list[Page]:
        """
        Create pages for an individual tag (with pagination if needed).

        Args:
            tag_slug: URL-safe tag slug
            tag_data: Dictionary containing tag name and pages

        Returns:
            List of generated tag pages
        """
        from bengal.utils.pagination import Paginator

        pages_to_create: list[Page] = []
        per_page = self.site.config.get("pagination", {}).get("per_page", 10)

        # Filter out any ineligible pages (defensive check)
        eligible_pages = [p for p in tag_data["pages"] if self._is_eligible_for_taxonomy(p)]

        # Create paginator
        paginator = Paginator(eligible_pages, per_page=per_page)

        # Create a page for each pagination page
        for page_num in range(1, paginator.num_pages + 1):
            spec = VirtualPageSpec(
                title=f"Posts tagged '{tag_data['name']}'",
                template="tag.html",
                page_type="tag",
                metadata={
                    "_tag": tag_data["name"],
                    "_tag_slug": tag_slug,
                    "_taxonomy_term": tag_slug,
                    "_posts": eligible_pages,
                    "_paginator": paginator,
                    "_page_num": page_num,
                },
            )

            tag_page = create_virtual_page(
                site=self.site,
                url_strategy=self.url_strategy,
                spec=spec,
                path_segments=("tags", tag_slug, f"page_{page_num}"),
                output_path=self.url_strategy.compute_tag_output_path(
                    tag_slug=tag_slug, page_num=page_num, site=self.site
                ),
                registry_owner="taxonomy",
                registry_priority=40,
            )
            pages_to_create.append(tag_page)

        return pages_to_create

    def _create_tag_pages_for_lang(
        self, tag_slug: str, tag_data: dict[str, Any], lang: str
    ) -> list[Page]:
        """Create tag pages for a specific language."""
        from bengal.utils.pagination import Paginator

        pages_to_create: list[Page] = []
        per_page = self.site.config.get("pagination", {}).get("per_page", 10)

        # Filter out any ineligible pages (defensive check)
        eligible_pages = [p for p in tag_data["pages"] if self._is_eligible_for_taxonomy(p)]
        paginator = Paginator(eligible_pages, per_page=per_page)

        for page_num in range(1, paginator.num_pages + 1):
            spec = VirtualPageSpec(
                title=f"Posts tagged '{tag_data['name']}'",
                template="tag.html",
                page_type="tag",
                metadata={
                    "_tag": tag_data["name"],
                    "_tag_slug": tag_slug,
                    "_taxonomy_term": tag_slug,
                    "_posts": eligible_pages,
                    "_paginator": paginator,
                    "_page_num": page_num,
                },
                lang=lang,
            )

            tag_page = create_virtual_page(
                site=self.site,
                url_strategy=self.url_strategy,
                spec=spec,
                path_segments=("tags", tag_slug, f"page_{page_num}"),
                output_path=self.url_strategy.compute_tag_output_path(
                    tag_slug=tag_slug, page_num=page_num, site=self.site
                ),
                registry_owner=None,  # Skip registry claim for lang-specific pages
            )
            pages_to_create.append(tag_page)

        return pages_to_create

    def generate_tag_pages(
        self, tags: list[str], selective: bool = False, context: BuildContext | None = None
    ) -> list[Page]:
        """
        Generate pages for a list of tags.

        Args:
            tags: List of tag slugs to generate pages for
            selective: If True, skip generation when tag count is below threshold
            context: Optional BuildContext for dependency injection

        Returns:
            List of generated tag pages
        """
        if context:
            self.threshold = getattr(context, "threshold", 20)  # DI from context
        if selective and len(tags) < self.threshold:
            return []  # Skip small

        # Always call for tests (configurable)
        if selective and self._is_test_mode():
            self.threshold = 0

        all_pages: list[Page] = []
        for tag_slug in tags:
            # Look up tag data from site.taxonomies
            tag_data = self.site.taxonomies.get("tags", {}).get(tag_slug)
            if tag_data:
                pages = self._create_tag_pages(tag_slug, tag_data)
                all_pages.extend(pages)
        return all_pages

    def _is_test_mode(self) -> bool:
        import os

        return os.getenv("PYTEST_CURRENT_TEST") is not None
