from __future__ import annotations

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,
    csrf_token,
    url_for,
    url_for_static_asset,
)

try:
    from jinja2 import Environment, FileSystemLoader, pass_context
    from jinja2 import TemplateNotFound as JinjaTemplateNotFound
except ImportError as e:
    raise MissingDependencyException("jinja2", extra="jinja") from e

if TYPE_CHECKING:
    from pathlib import Path

    from jinja2 import Template as JinjaTemplate

__all__ = ("JinjaTemplateEngine",)

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


class JinjaTemplateEngine(TemplateEngineProtocol["JinjaTemplate", Mapping[str, Any]]):
    """The engine instance."""

    def __init__(
        self,
        directory: Path | list[Path] | None = None,
        engine_instance: Environment | None = None,
    ) -> None:
        """Jinja-based TemplateEngine.

        Args:
            directory: Direct path or list of directory paths from which to serve templates.
            engine_instance: A jinja Environment instance.
        """

        super().__init__(directory, engine_instance)
        if directory and engine_instance:
            raise ImproperlyConfiguredException("You must provide either a directory or a jinja2 Environment instance.")
        if directory:
            loader = FileSystemLoader(searchpath=directory)
            self.engine = Environment(loader=loader, autoescape=True)
        elif engine_instance:
            self.engine = engine_instance
        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) -> JinjaTemplate:
        """Retrieve a template by matching its name (dotted path) with files in the directory or directories provided.

        Args:
            template_name: A dotted path

        Returns:
            JinjaTemplate instance

        Raises:
            TemplateNotFoundException: if no template is found.
        """
        try:
            return self.engine.get_template(name=template_name)
        except JinjaTemplateNotFound 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.engine.globals[key] = pass_context(template_callable)

    def render_string(self, template_string: str, context: Mapping[str, Any]) -> str:
        """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 = self.engine.from_string(template_string)
        return template.render(context)

    @classmethod
    def from_environment(cls, jinja_environment: Environment) -> JinjaTemplateEngine:
        """Create a JinjaTemplateEngine from an existing jinja Environment instance.

        Args:
            jinja_environment (jinja2.environment.Environment): A jinja Environment instance.

        Returns:
            JinjaTemplateEngine instance
        """
        return cls(directory=None, engine_instance=jinja_environment)
