"""Template rendering utilities for Mustache templates."""

from pathlib import Path
from typing import Any, Callable, Dict, Optional, TypeVar

import chevron
from chevron import ChevronError

from mcp_guide.core.mcp_log import get_logger
from mcp_guide.discovery.files import TEMPLATE_EXTENSIONS, FileInfo
from mcp_guide.render.context import TemplateContext
from mcp_guide.render.frontmatter import get_frontmatter_includes
from mcp_guide.render.functions import TemplateFunctions
from mcp_guide.render.partials import PartialNotFoundError, load_partial_content
from mcp_guide.result import Result
from mcp_guide.result_constants import ERROR_TEMPLATE, INSTRUCTION_VALIDATION_ERROR

logger = get_logger(__name__)

_VT = TypeVar("_VT")


class _TrackingDict(Dict[str, _VT]):
    """Dict subclass that records which keys were accessed (for chevron partial tracking)."""

    def __init__(self, *args: Any, **kwargs: Any) -> None:
        super().__init__(*args, **kwargs)
        self.accessed: dict[str, None] = {}

    def __getitem__(self, key: str) -> _VT:
        self.accessed[key] = None
        return super().__getitem__(key)

    def get(self, key: str, default: Any = None) -> Any:
        self.accessed[key] = None
        return super().get(key, default)

    def __contains__(self, key: object) -> bool:
        if isinstance(key, str):
            self.accessed[key] = None
        return super().__contains__(key)


def is_template_file(file_info: FileInfo) -> bool:
    """Check if FileInfo represents a template file.

    Args:
        file_info: FileInfo to check

    Returns:
        True if file has template extension
    """
    return str(file_info.path).endswith(TEMPLATE_EXTENSIONS)


def _safe_lambda(func: Callable[..., str]) -> Callable[..., str]:
    """Wrap lambda function to handle errors gracefully.

    Logs the full exception and returns a concise, user-safe error string that
    includes the exception type to aid debugging.
    """

    def wrapper(*args: Any, **kwargs: Any) -> str:
        try:
            return func(*args, **kwargs)
        except Exception as e:
            # Log full traceback for diagnostics
            logger.exception("Error while evaluating template lambda", exc_info=True)
            # Return a user-friendly error string with exception type preserved
            return f"[Template Error ({type(e).__name__}): {e}]"

    return wrapper


async def render_template_content(
    content: str,
    context: TemplateContext,
    file_path: str = "<template>",
    transient_fn: Optional[Callable[[TemplateContext], TemplateContext]] = None,
    partials: Optional[Dict[str, str]] = None,
    metadata: Optional[Dict[str, Any]] = None,
    base_dir: Optional[Path] = None,
) -> Result[tuple[str, list[Dict[str, Any]], list[str]]]:
    """Render template content with context.

    Args:
        content: Template content to render
        context: Template context data
        file_path: File path for error reporting
        transient_fn: Optional function to add transient data to context
        partials: Optional dictionary of partial templates
        metadata: Optional frontmatter metadata to merge into context

    Returns:
        Result with tuple of (rendered content, list of partial frontmatter)
    """
    try:
        # Process metadata (frontmatter) if provided
        render_context = context
        processed_partials: Dict[str, str] = partials or {}
        partial_frontmatter_list: list[Dict[str, Any]] = []
        # Maps partial name → its frontmatter (only for partials with content)
        partial_frontmatter_by_name: Dict[str, Dict[str, Any]] = {}

        if metadata:
            # Add frontmatter data to context
            metadata_context = TemplateContext(metadata)
            render_context = metadata_context.new_child(context)

            # Extract partials from frontmatter includes
            if includes := get_frontmatter_includes(metadata):
                logger.trace(f"Processing includes for template {file_path}: {includes}")
                try:
                    # Process includes and merge with existing partials
                    for include_path in includes:
                        partial_name = Path(include_path).stem
                        if partial_name.startswith("_"):
                            partial_name = partial_name[1:]

                        # Construct proper partial path with _ prefix
                        include_dir = Path(include_path).parent
                        partial_filename = f"_{partial_name}"
                        full_include_path = include_dir / partial_filename

                        # Load partial content using base directory
                        try:
                            # Build context for frontmatter requirements checking
                            context_dict = dict(render_context) if render_context else {}

                            if base_dir:
                                partial_content, partial_frontmatter = await load_partial_content(
                                    full_include_path, base_dir, context_dict
                                )
                            else:
                                # Fallback to file path parent if no base_dir provided
                                file_parent = Path(file_path).parent if file_path != "<template>" else Path.cwd()
                                partial_content, partial_frontmatter = await load_partial_content(
                                    full_include_path, file_parent, context_dict
                                )

                            processed_partials[partial_name] = partial_content
                            # Track frontmatter for partials that have content
                            if partial_frontmatter and partial_content:
                                partial_frontmatter_by_name[partial_name] = partial_frontmatter
                            logger.trace(f"Loaded partial '{partial_name}' from {include_path}")
                        except PartialNotFoundError as e:
                            logger.error(f"Partial template not found: {include_path} - {e}")
                        except (OSError, PermissionError) as e:
                            logger.error(f"Failed to read partial file {include_path}: {e}")
                        except ValueError as e:
                            logger.error(f"Invalid partial path {include_path}: {e}")

                except Exception as e:
                    logger.error(f"Error processing includes for {file_path}: {e}")

        # Apply transient function if provided
        final_context = transient_fn(render_context) if transient_fn else render_context

        # Create template functions and inject into context with error handling
        functions = TemplateFunctions(final_context)
        stem = Path(file_path).stem if file_path else "template"
        template_context = final_context.new_child(
            {
                "template_name": stem.lstrip("_"),
                "_error": functions._error,
                "format_date": _safe_lambda(functions.format_date),
                "truncate": _safe_lambda(functions.truncate),
                "highlight_code": _safe_lambda(functions.highlight_code),
                "pad_right": _safe_lambda(functions.pad_right),
                "contains": _safe_lambda(functions.contains),
                "equals": _safe_lambda(functions.equals),
                "time_ago": _safe_lambda(functions.time_ago),
                "resource": _safe_lambda(functions.resource),
            }
        )

        # Use a tracking dict so we know which partials chevron actually renders
        tracking_partials: _TrackingDict[str] = _TrackingDict(processed_partials)

        # Render template with Chevron (TemplateContext works as ChainMap)
        logger.trace(f"Rendering template {file_path} with partials: {list(processed_partials.keys())}")
        rendered = chevron.render(content, template_context, partials_dict=tracking_partials)
        logger.trace(f"Template {file_path} rendered content ({len(rendered)} chars): {rendered[:1024]}")

        # Only collect frontmatter from partials that were actually rendered
        partial_frontmatter_list = [
            partial_frontmatter_by_name[name]
            for name in tracking_partials.accessed
            if name in partial_frontmatter_by_name
        ]

        return Result.ok((rendered, partial_frontmatter_list, functions.errors))

    except ChevronError as e:
        # Enhanced Chevron-specific error handling with line context
        error_msg = str(e)
        line_context = _extract_line_context(content, error_msg)
        full_error = f"Template syntax error in {file_path}: {error_msg}\n{line_context}"

        logger.error(full_error)
        return Result.failure(
            error=full_error,
            error_type=ERROR_TEMPLATE,
            exception=e,
            instruction=f"Fix template syntax in {file_path}. Check for unclosed sections, mismatched tags, or invalid mustache syntax.",
        )
    except Exception as e:
        # General error handling for other exceptions
        logger.warning(f"Template rendering error in {file_path}: {str(e)}")
        return Result.failure(
            error=f"Template rendering failed for {file_path}: {str(e)}",
            error_type=ERROR_TEMPLATE,
            exception=e,
            instruction=INSTRUCTION_VALIDATION_ERROR,
        )


def _extract_line_context(content: str, error_msg: str) -> str:
    """Extract line context from template content based on error message."""
    import re

    # Try to extract line number from error message
    line_match = re.search(r"line (\d+)", error_msg, re.IGNORECASE)
    if not line_match:
        return ""

    line_num = int(line_match.group(1))
    lines = content.split("\n")

    if line_num < 1 or line_num > len(lines):
        return ""

    # Show context: 2 lines before, error line, 2 lines after
    start = max(0, line_num - 3)
    end = min(len(lines), line_num + 2)

    context_lines = []
    for i in range(start, end):
        prefix = ">>> " if i == line_num - 1 else "    "
        context_lines.append(f"{prefix}{i + 1:4d} | {lines[i]}")

    return "\n" + "\n".join(context_lines)
