# Copyright (C) 2024 Cetmix OÜ
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from jinja2 import Environment, Template, meta
from jinja2.exceptions import TemplateSyntaxError

from odoo import _, api, fields, models
from odoo.exceptions import UserError, ValidationError


class CxTowerTemplateMixin(models.AbstractModel):
    """Used to implement template rendering functions.
    Inherit in your model in case you want to render variable values in it.
    """

    _name = "cx.tower.template.mixin"
    _description = "Cetmix Tower template rendering mixin"

    code = fields.Text(help="This field will be rendered using variables")
    variable_ids = fields.Many2many(
        string="Variables",
        comodel_name="cx.tower.variable",
        compute="_compute_variable_ids",
        store=True,
        copy=False,
    )

    @classmethod
    def _get_depends_fields(cls):
        """
        Define dependent fields for the `variable_ids` computation.

        This method should be overridden in inheriting models to provide
        a list of fields that influence the computation of `variable_ids`.
        These fields are used in the `@api.depends` decorator to trigger
        recomputation when their values change.

        Returns:
            list: A list of field names (str) that are dependencies for
                  the `variable_ids` computation. Default is an empty list.

        Example:
            In a subclass, override as follows:
            >>> @classmethod
            >>> def _get_depends_fields(cls):
            >>>     return ["code", "path"]
        """
        return []

    @api.depends(lambda self: self._get_depends_fields())
    def _compute_variable_ids(self):
        """
        Compute the values of the `variable_ids`
        field based on model-specific dependencies.

        This method retrieves the dependent fields using `_get_depends_fields`
        and dynamically calculates the values of `variable_ids` using the
        `_prepare_variable_commands` method.

        If no dependent fields or relation parameters are defined, the field
        is reset to an empty list.

        Example:
            If dependent fields include `code` and `path`, and the model-specific
            logic links them to variables, this method will update the `variable_ids`
            field accordingly.

        Raises:
            ValidationError: If the field metadata is incorrectly defined or
                             missing required attributes.

        Returns:
            None: The field `variable_ids` is updated in-place for each record.
        """
        depends_fields = self._get_depends_fields()

        for record in self:
            if depends_fields:
                record.variable_ids = record._prepare_variable_commands(depends_fields)
            else:
                record.variable_ids = [(5, 0, 0)]

    def render_code(self, pythonic_mode=False, **kwargs):
        """Render record 'code' field using variables from kwargs
        Call to render recordset of the inheriting models
        Args:
            pythonic_mode (Bool): If True, all variables in kwargs are converted to
                                  strings and wrapped in double quotes.
                                  Default is False.
            **kwargs (dict): {variable: value, ...}
        Returns:
            dict {record_id: rendered_code, ...}
        """
        return {
            rec.id: self.render_code_custom(rec.code, pythonic_mode, **kwargs)
            for rec in self
        }

    def render_code_custom(self, code, pythonic_mode=False, **kwargs):
        """
        Render custom code using variables from kwargs

        This method renders a template string (code) using the variables provided
        in kwargs. If pythonic_mode is enabled, all variables are automatically
        converted to strings and enclosed in double quotes before rendering.

        Args:
            code (Text): code to render (eg 'some {{ custom }} text')
            pythonic_mode (Bool): If True, all variables in kwargs are converted to
                                  strings and wrapped in double quotes.
                                  Default is False.
            **kwargs (dict): {variable: value, ...}
        Returns:
            rendered_code (text): The resulting string after rendering the template with
                                  the provided variables.
        """
        try:
            if pythonic_mode:
                kwargs = {
                    key: self._make_value_pythonic(value)
                    for key, value in kwargs.items()
                }
            return Template(code, trim_blocks=True).render(kwargs)
        except Exception as e:
            raise UserError(str(e)) from e

    def get_variables(self):
        """Get the list of variables for templates
        Call to get variables for recordset of the inheriting models

        Returns:
            dict {'record_id': {variables}...}
                NB: 'record_id' is String
        """
        res = {}
        for rec in self:
            res[str(rec.id)] = self.get_variables_from_code(rec.code)
        return res

    def get_variables_from_code(self, code):
        """Get the list of variables for templates
        Call to get variables from custom code string

        Args:
            code (Text) custom code (eg 'Custom {{ var }} {{ var2 }} ...')
        Returns:
            variables (List) variables (eg ['var','var2',..])
        """
        env = Environment()
        try:
            ast = env.parse(code)
            undeclared_variables = meta.find_undeclared_variables(ast)
            return list(undeclared_variables) if undeclared_variables else []
        except TemplateSyntaxError as e:
            raise ValidationError(_("Variable syntax error: %s", e)) from e

    def _prepare_variable_commands(self, field_names, force_record=None):
        """
        Prepares commands to set variable references from the given fields.

        Args:
            field_names (list): List of field names to extract variable references from.
            force_record (record, optional): A record to use instead of the current one.

        Returns:
            list: An Odoo command to assign or clear variable references.
        """
        record = force_record or self
        record.ensure_one()

        all_references = set()
        for field_name in field_names:
            value = getattr(record, field_name, None)
            if value:
                all_references.update(self.get_variables_from_code(value))

        if all_references:
            variables = self.env["cx.tower.variable"].search(
                [("reference", "in", list(all_references))]
            )
            command = [(6, 0, variables.ids)]
        else:
            command = [(5, 0, 0)]

        return command

    def _make_value_pythonic(self, value):
        """Prepares value for use in 'pythonic' mode
            by enclosing strings into double quotes

        Args:
            value (Char): value to process

        Returns:
            Char: processed value
        """

        # Nothing to do here
        if isinstance(value, bool) or value is None:
            result = value

        # Handle nested dicts such as system variables
        elif isinstance(value, dict):
            result = {}
            for key, val in value.items():
                result.update({key: self._make_value_pythonic(val)})
        else:
            # Enclose in double quotes
            result = f'"{value}"'
        return result
