from __future__ import annotations

from functools import partial
from typing import TYPE_CHECKING, Any, Mapping, TypeVar

from typing_extensions import ParamSpec

from litestar.exceptions import ImproperlyConfiguredException, MissingDependencyException, TemplateNotFoundException
from litestar.template.base import (
    TemplateCallableType,
    TemplateEngineProtocol,
    TemplateProtocol,
    csrf_token,
    url_for,
    url_for_static_asset,
)

try:
    from mako.exceptions import TemplateLookupException as MakoTemplateNotFound  # type: ignore[import-untyped]
    from mako.lookup import TemplateLookup  # type: ignore[import-untyped]
    from mako.template import Template as _MakoTemplate  # type: ignore[import-untyped]
except ImportError as e:
    raise MissingDependencyException("mako") from e

if TYPE_CHECKING:
    from pathlib import Path

__all__ = ("MakoTemplate", "MakoTemplateEngine")

P = ParamSpec("P")
T = TypeVar("T")


class MakoTemplate(TemplateProtocol):
    """Mako template, implementing ``TemplateProtocol``"""

    def __init__(self, template: _MakoTemplate, template_callables: list[tuple[str, TemplateCallableType]]) -> None:
        """Initialize a template.

        Args:
            template: Base ``MakoTemplate`` used by the underlying mako-engine
            template_callables: List of callables passed to the template
        """
        super().__init__()
        self.template = template
        self.template_callables = template_callables

    def render(self, *args: Any, **kwargs: Any) -> str:
        """Render a template.

        Args:
            args: Positional arguments passed to the engines ``render`` function
            kwargs: Keyword arguments passed to the engines ``render`` function

        Returns:
            Rendered template as a string
        """
        for callable_key, template_callable in self.template_callables:
            kwargs_copy = {**kwargs}
            kwargs[callable_key] = partial(template_callable, kwargs_copy)

        return str(self.template.render(*args, **kwargs))


class MakoTemplateEngine(TemplateEngineProtocol[MakoTemplate, Mapping[str, Any]]):
    """Mako-based TemplateEngine."""

    def __init__(self, directory: Path | list[Path] | None = None, engine_instance: Any | None = None) -> None:
        """Initialize template engine.

        Args:
            directory: Direct path or list of directory paths from which to serve templates.
            engine_instance: A mako TemplateLookup instance.
        """
        super().__init__(directory, engine_instance)
        if directory and engine_instance:
            raise ImproperlyConfiguredException("You must provide either a directory or a mako TemplateLookup.")
        if directory:
            self.engine = TemplateLookup(
                directories=directory if isinstance(directory, (list, tuple)) else [directory], default_filters=["h"]
            )
        elif engine_instance:
            self.engine = engine_instance

        self._template_callables: list[tuple[str, TemplateCallableType]] = []
        self.register_template_callable(key="url_for_static_asset", template_callable=url_for_static_asset)
        self.register_template_callable(key="csrf_token", template_callable=csrf_token)
        self.register_template_callable(key="url_for", template_callable=url_for)

    def get_template(self, template_name: str) -> MakoTemplate:
        """Retrieve a template by matching its name (dotted path) with files in the directory or directories provided.

        Args:
            template_name: A dotted path

        Returns:
            MakoTemplate instance

        Raises:
            TemplateNotFoundException: if no template is found.
        """
        try:
            return MakoTemplate(
                template=self.engine.get_template(template_name), template_callables=self._template_callables
            )
        except MakoTemplateNotFound as exc:
            raise TemplateNotFoundException(template_name=template_name) from exc

    def register_template_callable(
        self, key: str, template_callable: TemplateCallableType[Mapping[str, Any], P, T]
    ) -> None:
        """Register a callable on the template engine.

        Args:
            key: The callable key, i.e. the value to use inside the template to call the callable.
            template_callable: A callable to register.

        Returns:
            None
        """
        self._template_callables.append((key, template_callable))

    def render_string(self, template_string: str, context: Mapping[str, Any]) -> str:  # pyright: ignore
        """Render a template from a string with the given context.

        Args:
            template_string: The template string to render.
            context: A dictionary of variables to pass to the template.

        Returns:
            The rendered template as a string.
        """
        template = _MakoTemplate(template_string)  # noqa: S702
        return template.render(**context)  # type: ignore[no-any-return]

    @classmethod
    def from_template_lookup(cls, template_lookup: TemplateLookup) -> MakoTemplateEngine:
        """Create a template engine from an existing mako TemplateLookup instance.

        Args:
            template_lookup: A mako TemplateLookup instance.

        Returns:
            MakoTemplateEngine instance
        """
        return cls(directory=None, engine_instance=template_lookup)
