# Jinja2 tools for cherrypy
# Copyright (C) 2021-2026 IKUS Software
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.

import importlib
import logging
import time

import cherrypy
import jinja2
from markupsafe import Markup

# Sentinel value
_UNDEFINED = object()

# Capture epoch time to invalidate cache of static file.
_cache_invalidate = int(time.time())

_JINJAX_TOKEN = '@@JINJAX-CATALOG-TOKEN@@'


class Jinja2Tool(cherrypy.Tool):
    """
    Jinja2 Tool for CherryPy.
    """

    def __init__(self):
        super().__init__('before_handler', self._wrap_handler, 'jinja2', priority=30)

    def _finalize_assets(self, catalog, html):
        """
        Replace the placeholder token in the rendered HTML with the fully
        formatted asset tags, then reset asset state for the next render.
        """
        # Skip this step if render_assets() has not been called.
        if hasattr(catalog, '_emit_assets_later') and getattr(catalog, '_emit_assets_later') is False:
            return html

        # Render Js and Css collected assets
        if hasattr(catalog, '_format_collected_assets'):
            # JinjaX > 0.60
            catalog._emit_assets_later = False
            assets_html = catalog._format_collected_assets()
            assets_html = str(assets_html).replace('<script type="module" ', '<script ')
            return str(html).replace(catalog._assets_placeholder, assets_html)
        else:
            # JinjaX<=0.60
            html_css = []
            rendered_urls = set()

            for url in catalog.collected_css:
                if url == _JINJAX_TOKEN:
                    continue
                if not url.startswith(("http://", "https://")):
                    full_url = f"{catalog.root_url}{url}"
                else:
                    full_url = url

                if full_url not in rendered_urls:
                    html_css.append(f'<link rel="stylesheet" href="{full_url}">')
                    rendered_urls.add(full_url)

            html_js = []
            for url in catalog.collected_js:
                if not url.startswith(("http://", "https://")):
                    full_url = f"{catalog.root_url}{url}"
                else:
                    full_url = url

                if full_url not in rendered_urls:
                    html_js.append(f'<script src="{full_url}"></script>')
                    rendered_urls.add(full_url)

            assets_html = Markup("\n".join(html_css + html_js))
            return str(html).replace(f'<link rel="stylesheet" href="/static/components/{_JINJAX_TOKEN}">', assets_html)

    def _wrap_handler(self, env, template, extra_processor=None, debug=False):

        def wrap(*args, **kwargs):
            # Call original handler
            context = self.oldhandler(*args, **kwargs)
            # Render template.
            return self.render_request(env=env, template=template, context=context, extra_processor=extra_processor)

        request = cherrypy.serving.request
        if request.handler is not None:
            # Replace request.handler with self
            if debug:
                cherrypy.log('replacing request handler', context='TOOLS.JINJA2', severity=logging.DEBUG)
            self.oldhandler = request.handler
            request.handler = wrap

    def create_env(self, package_name, filters={}, globals={}, tests={}):
        """
        Utility function used to create a default Jinja2 environment with good default.
        """
        env = jinja2.Environment(
            loader=jinja2.PackageLoader(package_name),
            auto_reload=True,
            autoescape=True,
            trim_blocks=True,
            lstrip_blocks=True,
        )
        # Param to bust caches
        env.globals['cache_invalidate'] = _cache_invalidate

        # Enable translation if available
        if hasattr(cherrypy.tools, 'i18n'):
            from .i18n import (
                format_date,
                format_datetime,
                get_language_name,
                get_timezone_name,
                get_translation,
                list_available_locales,
                list_available_timezones,
                ugettext,
                ungettext,
            )

            env.add_extension('jinja2.ext.i18n')
            env.install_gettext_callables(ugettext, ungettext, newstyle=True)
            env.filters['format_date'] = format_date
            env.filters['format_datetime'] = format_datetime
            env.globals['get_language_name'] = get_language_name
            env.globals['get_translation'] = get_translation
            env.globals['list_available_locales'] = list_available_locales
            env.globals['list_available_timezones'] = list_available_timezones
            env.globals['get_timezone_name'] = get_timezone_name

        # Update globals, filters and tests
        env.globals.update(globals)
        env.filters.update(filters)
        env.tests.update(tests)

        # Enable JinjaX if available
        try:
            import jinjax

            env.add_extension(jinjax.JinjaX)
            catalog = jinjax.Catalog(jinja_env=env, root_url="/static/components/")
            catalog.add_folder(importlib.resources.files(package_name) / 'components')
            catalog.add_folder(importlib.resources.files(__package__) / '..' / 'components')
        except ImportError:
            pass

        return env

    def render_request(self, template, context={}, env=_UNDEFINED, extra_processor=_UNDEFINED):
        """
        Render template for a given cherrypy request.
        """
        request = cherrypy.serving.request
        if env is _UNDEFINED:
            env = request.config.get('tools.jinja2.env')
        if extra_processor is _UNDEFINED:
            extra_processor = request.config.get('tools.jinja2.extra_processor')
        # Execute extra processor if defined.
        new_context = {}
        if extra_processor:
            new_context.update(extra_processor())
        new_context.update(context)
        # Render templates
        return self.render(env=env, template=template, context=new_context)

    def render(self, env, template, context={}):
        """
        Lower level function used to render a template using the given jinja2 environment, template(s) and variable context.
        """
        # Get the right templates
        if isinstance(template, (list, tuple)):
            names = [t.format(**context) for t in template]
            tmpl = env.select_template(names)
        else:
            tmpl = env.get_template(template)

        # Monkey patch JinjaX catalog.render_assets() for JinjaX <= 0.60
        catalog = env.globals.get('catalog')
        if catalog:
            catalog.collected_css = []
            catalog.collected_js = []
            if not hasattr(catalog, '_assets_placeholder'):
                # JinjaX <= 0.60
                # Leave a token to be replace.
                catalog.collected_css = [_JINJAX_TOKEN]

        # Render template
        out = tmpl.render(context)

        # With JinjaX finalise render_assets().
        if catalog:
            return self._finalize_assets(catalog=catalog, html=out)
        return out


cherrypy.tools.jinja2 = Jinja2Tool()
