"""Entry point for the usage of Cheetah templating within Galaxy."""

import sys
import traceback
from typing import (
    Optional,
    Union,
)

from Cheetah.Compiler import Compiler
from Cheetah.NameMapper import NotFound
from Cheetah.Parser import ParseError
from Cheetah.Template import Template
from packaging.version import Version

from galaxy.util.tree_dict import TreeDict
from . import unicodify

try:
    from lib2to3.refactor import RefactoringTool
except ImportError:
    # Either Python 3.13 or Debian(<=12)/Ubuntu(<=24.10) without the
    # python3-lib2to3 package
    import fissix
    from fissix import (
        fixes as fissix_fixes,
        pgen2 as fissix_pgen2,
        refactor as fissix_refactor,
    )

    sys.modules["lib2to3"] = fissix
    sys.modules["lib2to3.fixes"] = fissix_fixes
    sys.modules["lib2to3.pgen2"] = fissix_pgen2
    sys.modules["lib2to3.refactor"] = fissix_refactor

    from lib2to3.refactor import RefactoringTool

try:
    from past.translation import myfixes

    # Skip libpasteurize fixers, which make sure code is py2 and py3 compatible.
    # This is not needed, we only translate code on py3.
    myfixes = [f for f in myfixes if not f.startswith("libpasteurize")]
except ImportError:
    # future is not installed, so the libfuturize fixes are not available and
    # we can only use the ones from lib2to3/fissix.
    myfixes = [
        "lib2to3.fixes.fix_reduce",
        "lib2to3.fixes.fix_xreadlines",
        "lib2to3.fixes.fix_types",
        "lib2to3.fixes.fix_exec",
        "lib2to3.fixes.fix_repr",
        "lib2to3.fixes.fix_exitfunc",
        "lib2to3.fixes.fix_idioms",
        "lib2to3.fixes.fix_throw",
        "lib2to3.fixes.fix_tuple_params",
        "lib2to3.fixes.fix_has_key",
        "lib2to3.fixes.fix_standarderror",
        "lib2to3.fixes.fix_ne",
        "lib2to3.fixes.fix_ws_comma",
        "lib2to3.fixes.fix_intern",
        "lib2to3.fixes.fix_paren",
        "lib2to3.fixes.fix_funcattrs",
        "lib2to3.fixes.fix_methodattrs",
        "lib2to3.fixes.fix_isinstance",
        "lib2to3.fixes.fix_except",
        "lib2to3.fixes.fix_apply",
        "lib2to3.fixes.fix_renames",
        "lib2to3.fixes.fix_sys_exc",
        "lib2to3.fixes.fix_numliterals",
        "lib2to3.fixes.fix_filter",
        "lib2to3.fixes.fix_getcwdu",
        "lib2to3.fixes.fix_long",
        "lib2to3.fixes.fix_operator",
        "lib2to3.fixes.fix_itertools_imports",
        "lib2to3.fixes.fix_raw_input",
        "lib2to3.fixes.fix_map",
        "lib2to3.fixes.fix_zip",
        "lib2to3.fixes.fix_next",
        "lib2to3.fixes.fix_dict",
        "lib2to3.fixes.fix_nonzero",
        "lib2to3.fixes.fix_itertools",
    ]

refactoring_tool = RefactoringTool(myfixes, {"print_function": True})


class InputNotFoundSyntaxError(SyntaxError):
    pass


class FixedModuleCodeCompiler(Compiler):
    module_code = None

    def getModuleCode(self):
        self._moduleDef = self.module_code
        return self._moduleDef


def create_compiler_class(module_code):
    class CustomCompilerClass(FixedModuleCodeCompiler):
        pass

    CustomCompilerClass.module_code = module_code
    return CustomCompilerClass


def fill_template(
    template_text,
    context=None,
    retry=10,
    compiler_class=Compiler,
    first_exception=None,
    futurized=False,
    python_template_version: Optional[Union[str, Version]] = "3",
    **kwargs,
):
    """Fill a cheetah template out for specified context.

    If template_text is None, an exception will be thrown, if context
    is None (the default) - keyword arguments to this function will be used
    as the context.
    """
    if template_text is None:
        raise TypeError("Template text specified as None to fill_template.")
    if not context:
        context = kwargs
    if python_template_version is None:
        python_template_version = Version("3")
    elif isinstance(python_template_version, str):
        python_template_version = Version(python_template_version)
    try:
        klass = Template.compile(source=template_text, compilerClass=compiler_class)
    except ParseError as e:
        # Might happen on invalid syntax within a cheetah statement, like `#if $smxsize <> 128.0`
        if first_exception is None:
            first_exception = e
        if python_template_version.release[0] < 3 and retry > 0:
            module_code = Template.compile(
                source=template_text, compilerClass=compiler_class, returnAClass=False
            ).decode("utf-8")
            module_code = futurize_preprocessor(module_code)
            compiler_class = create_compiler_class(module_code)
            return fill_template(
                template_text=template_text,
                context=context,
                retry=retry - 1,
                compiler_class=compiler_class,
                first_exception=first_exception,
                python_template_version=python_template_version,
            )
        raise first_exception or e
    t = klass(searchList=[context])
    try:
        return unicodify(t, log_exception=False)
    except (NotFound, InputNotFoundSyntaxError) as e:
        if first_exception is None:
            first_exception = e
        if not isinstance(context, TreeDict):
            masked_input = None
            if "input" in context and callable(context["input"]):
                masked_input = context.pop("input", None)
            context = TreeDict(context)
            if "input" not in context and masked_input:
                context["input"] = masked_input
        tb = e.__traceback__
        if retry > 0:
            if python_template_version.release[0] < 3:
                last_stack = traceback.extract_tb(tb)[-1]
                if last_stack.name == "<listcomp>" and last_stack.lineno:
                    # On python 3 list, dict and set comprehensions as well as generator expressions
                    # have their own local scope, which prevents accessing frame variables in cheetah.
                    # We can work around this by replacing `$var` with `var`, but we only do this for
                    # list comprehensions, as this has never worked for dict or set comprehensions or
                    # generator expressions in Cheetah.
                    var_not_found = e.args[0].split("'")[1]
                    replace_str = f'VFFSL(SL,"{var_not_found}",True)'
                    lineno = last_stack.lineno - 1
                    module_code = t._CHEETAH_generatedModuleCode.splitlines()
                    module_code[lineno] = module_code[lineno].replace(replace_str, var_not_found)
                    module_code = "\n".join(module_code)
                    compiler_class = create_compiler_class(module_code)
            return fill_template(
                template_text=template_text,
                context=context,
                retry=retry - 1,
                compiler_class=compiler_class,
                first_exception=first_exception,
                python_template_version=python_template_version,
            )
        raise first_exception or e
    except Exception as e:
        if first_exception is None:
            first_exception = e
        if python_template_version.release[0] < 3 and not futurized:
            # Possibly an error caused by attempting to run python 2
            # template code on python 3. Run the generated module code
            # through futurize and hope for the best.
            module_code = t._CHEETAH_generatedModuleCode
            module_code = futurize_preprocessor(module_code)
            compiler_class = create_compiler_class(module_code)
            return fill_template(
                template_text=template_text,
                context=context,
                retry=retry,
                compiler_class=compiler_class,
                first_exception=first_exception,
                futurized=True,
                python_template_version=python_template_version,
            )
        raise first_exception or e


def futurize_preprocessor(source):
    source = str(refactoring_tool.refactor_string(source, name="auto_translate_cheetah"))
    # libfuturize.fixes.fix_unicode_keep_u' breaks from Cheetah.compat import unicode
    source = source.replace("from Cheetah.compat import str", "from Cheetah.compat import unicode")
    return source
