"""
Space Rendering Service.

Orchestrates transclusion processing, rendering, and caching for
Information Space documents.
"""

import logging
from pathlib import Path
from typing import Dict, Any, Optional, List, Set

from .base import SpaceRenderer, RenderConfig, RenderResult, RenderFormat, CompositeRenderer
from .html_renderer import MarkdownToHTMLRenderer
from ..transclusion import (
    SpaceTransclusionContext,
    ReferenceGraph,
    RenderCache,
    CacheInvalidator,
    CacheEntry,
)
from ..events import EventBus, SpaceEventType, SpaceEvent
from ..repositories.interfaces import IVariableRepository, IReferenceRepository

logger = logging.getLogger(__name__)


class SpaceRenderingService:
    """
    Service for rendering Information Space documents.

    Orchestrates:
    - Transclusion resolution
    - Content rendering to target format
    - Render caching with automatic invalidation
    - Event emission for render lifecycle
    """

    def __init__(
        self,
        renderer: Optional[SpaceRenderer] = None,
        render_cache: Optional[RenderCache] = None,
        reference_graph: Optional[ReferenceGraph] = None,
        event_bus: Optional[EventBus] = None,
        variable_repo: Optional[IVariableRepository] = None,
        reference_repo: Optional[IReferenceRepository] = None,
    ):
        """
        Initialize the rendering service.

        Args:
            renderer: Renderer for output format (defaults to HTML)
            render_cache: Cache for rendered output
            reference_graph: Graph for dependency tracking
            event_bus: Event bus for notifications
            variable_repo: Repository for transclusion variables
            reference_repo: Repository for transclusion references
        """
        self.renderer = renderer or MarkdownToHTMLRenderer()
        self.render_cache = render_cache or RenderCache()
        self.reference_graph = reference_graph or ReferenceGraph()
        self.event_bus = event_bus
        self.variable_repo = variable_repo
        self.reference_repo = reference_repo

        # Set up cache invalidator if event bus available
        self._cache_invalidator: Optional[CacheInvalidator] = None
        if event_bus:
            self._cache_invalidator = CacheInvalidator(
                cache=self.render_cache,
                reference_graph=self.reference_graph,
                event_bus=event_bus,
                transitive=True,
            )

    def render_document(
        self,
        content: str,
        document_id: str,
        space_id: str,
        base_path: Optional[Path] = None,
        variables: Optional[Dict[str, Any]] = None,
        force_refresh: bool = False,
    ) -> RenderResult:
        """
        Render a document with transclusion processing.

        Args:
            content: Raw markdown content
            document_id: Document identifier
            space_id: Space identifier
            base_path: Base path for relative file resolution
            variables: Variables for transclusion
            force_refresh: Skip cache and re-render

        Returns:
            RenderResult with rendered content
        """
        # Check cache first (unless force refresh)
        content_hash = RenderResult.compute_hash(content)

        if not force_refresh:
            cached = self._get_cached_if_valid(document_id, content_hash)
            if cached:
                logger.debug(f"Cache hit for document {document_id}")
                self._emit_event(
                    SpaceEventType.RENDER_COMPLETED,
                    space_id,
                    {"document_id": document_id, "cached": True},
                )
                return self._cache_entry_to_result(cached, space_id)

        logger.debug(f"Rendering document {document_id}")
        self._emit_event(
            SpaceEventType.RENDER_STARTED,
            space_id,
            {"document_id": document_id},
        )

        try:
            # Create transclusion context
            context = SpaceTransclusionContext(
                space_id=space_id,
                base_path=base_path,
                variables=variables,
                variable_repo=self.variable_repo,
            )
            context.set_current_document(document_id)

            # Process transclusions (this is a placeholder - actual transclusion
            # processing would use TransclusionEngine from packaging)
            resolved_content = self._process_transclusions(content, context)

            # Get tracked references for dependency graph
            references = context.get_tracked_references()
            self._update_reference_graph(document_id, references, space_id)

            # Render to target format
            dependencies = {ref[1] for ref in references}
            result = self.renderer.render(
                content=resolved_content,
                document_id=document_id,
                space_id=space_id,
                dependencies=dependencies,
            )

            # Cache the result
            self._cache_result(result)

            # Emit success event
            self._emit_event(
                SpaceEventType.RENDER_COMPLETED,
                space_id,
                {
                    "document_id": document_id,
                    "cached": False,
                    "dependencies": list(dependencies),
                },
            )

            return result

        except Exception as e:
            logger.error(f"Render failed for {document_id}: {e}")
            self._emit_event(
                SpaceEventType.RENDER_FAILED,
                space_id,
                {"document_id": document_id, "error": str(e)},
            )
            raise

    def render_documents(
        self,
        documents: List[Dict[str, Any]],
        space_id: str,
        base_path: Optional[Path] = None,
        variables: Optional[Dict[str, Any]] = None,
        force_refresh: bool = False,
    ) -> Dict[str, RenderResult]:
        """
        Render multiple documents.

        Args:
            documents: List of dicts with 'id' and 'content' keys
            space_id: Space identifier
            base_path: Base path for file resolution
            variables: Shared variables
            force_refresh: Skip cache

        Returns:
            Dict mapping document_id to RenderResult
        """
        results = {}

        for doc in documents:
            doc_id = doc["id"]
            content = doc["content"]

            try:
                result = self.render_document(
                    content=content,
                    document_id=doc_id,
                    space_id=space_id,
                    base_path=base_path,
                    variables=variables,
                    force_refresh=force_refresh,
                )
                results[doc_id] = result
            except Exception as e:
                logger.error(f"Failed to render {doc_id}: {e}")
                # Continue with other documents

        return results

    def get_cached_render(self, document_id: str) -> Optional[CacheEntry]:
        """
        Get cached render for a document.

        Args:
            document_id: Document ID

        Returns:
            CacheEntry if cached, None otherwise
        """
        return self.render_cache.get(document_id)

    def invalidate_document(self, document_id: str, space_id: str) -> Set[str]:
        """
        Invalidate cache for a document and its dependents.

        Args:
            document_id: Document to invalidate
            space_id: Space ID

        Returns:
            Set of invalidated document IDs
        """
        if self._cache_invalidator:
            return self._cache_invalidator.invalidate_for_document(document_id, space_id)
        else:
            # Manual invalidation
            to_invalidate = {document_id}
            to_invalidate.update(
                self.reference_graph.get_transitive_dependents(document_id)
            )
            self.render_cache.invalidate_many(to_invalidate)
            return to_invalidate

    def invalidate_space(self, space_id: str) -> int:
        """
        Invalidate all cached renders for a space.

        Args:
            space_id: Space ID

        Returns:
            Number of entries invalidated
        """
        return self.render_cache.invalidate_space(space_id)

    def get_render_statistics(self, space_id: str) -> Dict[str, Any]:
        """
        Get rendering statistics for a space.

        Args:
            space_id: Space ID

        Returns:
            Statistics dict
        """
        cached_docs = self.render_cache.get_cached_documents(space_id)
        graph_docs = self.reference_graph.get_documents_in_space(space_id)

        return {
            "cached_documents": len(cached_docs),
            "tracked_documents": len(graph_docs),
            "render_format": self.renderer.config.format.value,
            "theme": self.renderer.config.theme.name,
        }

    def _process_transclusions(
        self, content: str, context: SpaceTransclusionContext
    ) -> str:
        """
        Process transclusion directives in content.

        This is a simplified implementation. Full transclusion processing
        would use the TransclusionEngine from markitect.packaging.transclusion.
        """
        # Substitute variables
        resolved = context.substitute_variables(content)

        # Track any {{include:path}} references
        import re

        include_pattern = r'\{\{include:([^}]+)\}\}'
        for match in re.finditer(include_pattern, content):
            target_path = match.group(1).strip()
            context.track_reference(target_path)

        # Note: Full transclusion processing would:
        # 1. Parse directives using DirectiveParser
        # 2. Resolve file includes recursively
        # 3. Handle conditionals
        # For now, we just do variable substitution

        return resolved

    def _update_reference_graph(
        self,
        document_id: str,
        references: List[tuple],
        space_id: str,
    ) -> None:
        """Update the reference graph with document dependencies."""
        # Clear old references from this document
        self.reference_graph.clear_references_from(document_id)

        # Add new references
        for source, target in references:
            self.reference_graph.add_reference(source, target, space_id)

    def _get_cached_if_valid(
        self, document_id: str, content_hash: str
    ) -> Optional[CacheEntry]:
        """Get cached entry if still valid."""
        entry = self.render_cache.get(document_id)
        if entry and entry.content_hash == content_hash:
            return entry
        return None

    def _cache_result(self, result: RenderResult) -> None:
        """Cache a render result."""
        self.render_cache.put(
            document_id=result.document_id,
            space_id=result.space_id,
            content_hash=result.source_hash,
            rendered_content=result.content,
            dependencies=result.dependencies,
        )

    def _cache_entry_to_result(
        self, entry: CacheEntry, space_id: str
    ) -> RenderResult:
        """Convert cache entry to render result."""
        return RenderResult(
            content=entry.rendered_content,
            format=self.renderer.config.format,
            content_hash=RenderResult.compute_hash(entry.rendered_content),
            source_hash=entry.content_hash,
            document_id=entry.document_id,
            space_id=space_id,
            dependencies=entry.dependencies,
        )

    def _emit_event(
        self, event_type: SpaceEventType, space_id: str, payload: Dict[str, Any]
    ) -> None:
        """Emit an event if event bus is available."""
        if not self.event_bus:
            return

        event = SpaceEvent(
            event_type=event_type,
            space_id=space_id,
            payload=payload,
        )
        self.event_bus.emit(event)


class SpaceRenderingServiceBuilder:
    """Builder for creating configured SpaceRenderingService instances."""

    def __init__(self):
        """Initialize builder with defaults."""
        self._renderer: Optional[SpaceRenderer] = None
        self._render_cache: Optional[RenderCache] = None
        self._reference_graph: Optional[ReferenceGraph] = None
        self._event_bus: Optional[EventBus] = None
        self._variable_repo: Optional[IVariableRepository] = None
        self._reference_repo: Optional[IReferenceRepository] = None

    def with_renderer(self, renderer: SpaceRenderer) -> "SpaceRenderingServiceBuilder":
        """Set the renderer."""
        self._renderer = renderer
        return self

    def with_html_renderer(
        self, config: Optional[RenderConfig] = None
    ) -> "SpaceRenderingServiceBuilder":
        """Use HTML renderer with optional config."""
        self._renderer = MarkdownToHTMLRenderer(config)
        return self

    def with_cache(self, cache: RenderCache) -> "SpaceRenderingServiceBuilder":
        """Set the render cache."""
        self._render_cache = cache
        return self

    def with_reference_graph(
        self, graph: ReferenceGraph
    ) -> "SpaceRenderingServiceBuilder":
        """Set the reference graph."""
        self._reference_graph = graph
        return self

    def with_event_bus(self, bus: EventBus) -> "SpaceRenderingServiceBuilder":
        """Set the event bus."""
        self._event_bus = bus
        return self

    def with_variable_repo(
        self, repo: IVariableRepository
    ) -> "SpaceRenderingServiceBuilder":
        """Set the variable repository."""
        self._variable_repo = repo
        return self

    def with_reference_repo(
        self, repo: IReferenceRepository
    ) -> "SpaceRenderingServiceBuilder":
        """Set the reference repository."""
        self._reference_repo = repo
        return self

    def build(self) -> SpaceRenderingService:
        """Build the configured service."""
        return SpaceRenderingService(
            renderer=self._renderer,
            render_cache=self._render_cache,
            reference_graph=self._reference_graph,
            event_bus=self._event_bus,
            variable_repo=self._variable_repo,
            reference_repo=self._reference_repo,
        )
