"""Annotate variable

Created by:
EOLE (http://eole.orion.education.fr)
Copyright (C) 2005-2018

Forked by:
Cadoles (http://www.cadoles.com)
Copyright (C) 2019-2021

Silique (https://www.silique.fr)
Copyright (C) 2022-2026

This program is free software: you can redistribute it and/or modify it
under the terms of the GNU Lesser 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 Lesser General Public License for more
details.

You should have received a copy of the GNU Lesser General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
"""

from tiramisu.error import display_list
from rougail.i18n import _
from rougail.utils import calc_multi_for_type_variable
from rougail.error import DictConsistencyError
from rougail.convert.object_model import (
    Calculation,
    VariableCalculation,
    VariableParam,
    IndexCalculation,
    JinjaCalculation,
)
from rougail.tiramisu import display_xmlfiles, RENAME_TYPE

from warnings import warn


class Walk:
    """Walk to objectspace to find variable or family"""

    objectspace = None

    def get_variables(self):
        """Iter all variables from the objectspace"""
        for path in self.objectspace.variables:
            yield self.objectspace.paths[path]

    def get_families(self):
        """Iter all families from the objectspace"""
        for path in self.objectspace.families:
            yield self.objectspace.paths[path]


class Annotator(Walk):  # pylint: disable=R0903
    """Annotate variable"""

    level = 30

    def __init__(
        self,
        objectspace,
        *args,
    ):
        if not objectspace.paths:
            return
        self.objectspace = objectspace
        # default type inference from a default value with :term:`basic types`
        self.basic_types = {
            str: "string",
            int: "integer",
            bool: "boolean",
            float: "float",
        }
        self.verify_secret_managers()
        self.verify_choices()
        self.convert_variable()
        self.convert_test()
        self.convert_examples()
        self.convert_help()

    def verify_secret_managers(self):
        for variable in self.get_variables():
            if not variable.secret_manager:
                continue
            path = variable.path
            if variable.type not in ["unix_user", "secret"]:
                msg = _(
                    'only "unix_user" or "secret" variable type can have "secret_manager" attribute, but "{0}" has type "{1}"'
                )
                raise DictConsistencyError(
                    msg.format(path, variable.type), 56, variable.xmlfiles
                )
            if variable.multi and path not in self.objectspace.leaders:
                msg = _(
                    'the variable "{0}" has attribute "secret_manager" but is a multi variable'
                )
                raise DictConsistencyError(msg.format(path), 57, variable.xmlfiles)
            if variable.default is not None:
                msg = _(
                    'the variable "{0}" has attribute "secret_manager" so must not have default value'
                )
                raise DictConsistencyError(msg.format(path), 59, variable.xmlfiles)
            self.objectspace.informations.add(path, "secret_manager", True)

    def convert_variable(self):
        """convert variable"""
        for variable in self.get_variables():
            if variable.type == "symlink":
                continue
            if variable.version != "1.0":
                self._convert_variable_inference(variable)
            self._convert_variable_multi(variable)
        for variable in self.get_variables():
            if variable.type == "symlink":
                continue
            if variable.type in RENAME_TYPE:
                warning = f'the variable "{ variable.path }" has a depreciated type "{variable.type}", please use "{RENAME_TYPE[variable.type]}" instead in {display_xmlfiles(variable.xmlfiles)}'
                warn(
                    warning,
                    DeprecationWarning,
                )
                variable.type = RENAME_TYPE[variable.type]
            if variable.type == "cidr":
                warning = f'the variable "{ variable.path }" has a depreciated type "{variable.type}", please use type "ip" with attribute cidr=True instead in {display_xmlfiles(variable.xmlfiles)}'
                warn(
                    warning,
                    DeprecationWarning,
                )
            if variable.type == "network_cidr":
                warning = f'the variable "{ variable.path }" has a depreciated type "{variable.type}", please use type "network" with attribute cidr=True instead in {display_xmlfiles(variable.xmlfiles)}'
                warn(
                    warning,
                    DeprecationWarning,
                )
            self.objectspace.informations.add(
                variable.path, "ymlfiles", variable.xmlfiles
            )
            if variable.version != "1.0" and variable.type is None:
                if isinstance(variable.default, IndexCalculation):
                    variable.type = "integer"
                elif isinstance(variable.default, VariableCalculation):
                    calculated_variable_path, calculated_variable, identifier = (
                        variable.default.get_variable(self.objectspace, variable.path)
                    )
                    if calculated_variable is not None:
                        variable = self._default_variable_copy_informations(
                            variable, calculated_variable
                        )
            self._convert_variable(variable)
            if variable.multi and variable.params:
                indexes_to_remove = []
                for idx, param in enumerate(variable.params):
                    if param.key == "multi_length":
                        jinja = f'{{% if var | length != {param.value} %}}{_("{0} values needed, but there are {{{{ var | length }}}}").format(param.value)}{{% endif %}}'
                        description = _("needs exactly {0} values").format(param.value)
                    elif param.key == "multi_max_length":
                        jinja = f'{{% if var | length > {param.value} %}}{_("a maximum of {0} values are needed, but there are {{{{ var | length }}}}").format(param.value)}{{% endif %}}'
                        description = _("needs a maximum of {0} values").format(
                            param.value
                        )
                    elif param.key == "multi_min_length":
                        jinja = f'{{% if var | length < {param.value} %}}{_("a minimum of {0} values are needed, but there are {{{{ var | length }}}}").format(param.value)}{{% endif %}}'
                        description = _("needs a minimum of {0} values").format(
                            param.value
                        )
                    else:
                        continue
                    indexes_to_remove.append(idx)
                    if variable.validators is None:
                        variable.validators = []

                    variable.validators.append(
                        JinjaCalculation(
                            inside_list=True,
                            version=variable.version,
                            xmlfiles=variable.xmlfiles,
                            attribute_name="validators",
                            namespace=variable.namespace,
                            jinja=jinja,
                            description=description,
                            params=[
                                VariableParam(
                                    variable.path,
                                    "validators",
                                    False,
                                    variable.xmlfiles,
                                    key="var",
                                    namespace=variable.namespace,
                                    type="variable",
                                    variable=variable.path,
                                    whole=True,
                                )
                            ],
                        )
                    )
                for idx in reversed(indexes_to_remove):
                    variable.params.pop(idx)
            if variable.secret_manager:
                self.objectspace.properties.add(variable.path, "novalidator", True)

    def _convert_variable_inference(
        self,
        variable,
    ) -> None:
        # variable has no type
        if variable.type is not None:
            return
        ## choice type inference from the `choices` attribute
        # if variable.choices is not None:
        #    variable.type = "choice"
        # elif variable.regexp is not None:
        #    variable.type = "regexp"
        if variable.default not in [None, []]:
            if isinstance(variable.default, list):
                tested_value = variable.default[0]
            else:
                tested_value = variable.default
            variable.type = self.basic_types.get(type(tested_value), None)

    def _convert_variable_multi(
        self,
        variable,
    ) -> None:
        # variable has no multi attribute
        if variable.multi is not None:
            return
        if variable.path in self.objectspace.leaders:
            variable.multi = self.objectspace.multis[variable.path] = True
        elif variable.version != "1.0" and isinstance(
            variable.default, VariableCalculation
        ):
            calculated_variable_path, calculated_variable, identifier = (
                variable.default.get_variable(self.objectspace, variable.path)
            )
            if calculated_variable is not None:
                if calculated_variable.multi is None:
                    if (
                        isinstance(calculated_variable.default, VariableCalculation)
                        and variable.path == calculated_variable.path
                    ):
                        msg = _(
                            'the "{0}" default value is a calculation with itself'
                        ).format(variable.path)
                        raise DictConsistencyError(msg, 75, variable.xmlfiles)
                    self._convert_variable_multi(calculated_variable)
                identifier_is_a_calculation = False
                if isinstance(variable.default, Calculation):
                    identifier_is_a_calculation = isinstance(
                        variable.default.identifier, Calculation
                    )
                variable.multi = calc_multi_for_type_variable(
                    variable,
                    calculated_variable_path,
                    calculated_variable,
                    identifier_is_a_calculation,
                    self.objectspace,
                )[1]
                if (
                    calculated_variable.path in self.objectspace.followers
                    and variable.mandatory is calculated_variable.mandatory is False
                    and calculated_variable.path.rsplit(".", 1)[0]
                    != variable.path.rsplit(".", 1)[0]
                ):
                    variable.empty = False
        else:
            variable.multi = isinstance(variable.default, list)

    def _default_variable_copy_informations(
        self,
        variable,
        calculated_variable,
    ) -> None:
        # copy type and params
        calculated_type = calculated_variable.__class__
        if variable.__class__ == calculated_type:
            variable.type = calculated_variable.type
        else:
            data = dict(variable)
            data["type"] = calculated_variable.type
            variable = calculated_type(**data)
            self.objectspace.paths.replace(data["path"], variable)
        if variable.params is None and calculated_variable.params is not None:
            variable.params = calculated_variable.params
        if variable.type == "choice" and variable.choices is None:
            variable.choices = calculated_variable.choices
            if isinstance(variable.choices, VariableCalculation):
                variable.choices = variable.choices.model_copy()
                variable.choices.variable = self.objectspace.paths.get_full_path(
                    variable.choices.variable,
                    calculated_variable.path,
                )
        if variable.type == "regexp" and variable.regexp is None:
            variable.regexp = calculated_variable.regexp
        return variable

    def _convert_variable(
        self,
        variable: dict,
    ) -> None:
        # variable without description: description is the name
        if not variable.description:
            variable.description = variable.name
            self.objectspace.forced_descriptions.append(variable.path)
        if variable.type is None:
            variable.type = "string"
        self.objectspace.informations.add(variable.path, "type", variable.type)
        if variable.path in self.objectspace.followers:
            if not variable.multi:
                self.objectspace.multis[variable.path] = True
            else:
                self.objectspace.multis[variable.path] = "submulti"
        elif variable.multi:
            self.objectspace.multis[variable.path] = True
        #        if variable.regexp is not None and variable.type != "regexp":
        #            msg = _(
        #                'the variable "{0}" has regexp attribut but has not the "regexp" type'
        #            ).format(variable.path)
        #            raise DictConsistencyError(msg, 37, variable.xmlfiles)
        if variable.mandatory is None:
            variable.mandatory = True

    def convert_test(self):
        """Convert variable tests value"""
        for variable in self.get_variables():
            if variable.type == "symlink":
                continue
            if variable.test is None:
                continue
            self.objectspace.informations.add(
                variable.path, "test", tuple(variable.test)
            )

    def convert_examples(self):
        """Convert variable tests value"""
        for variable in self.get_variables():
            if variable.type == "symlink":
                continue
            if variable.examples is None:
                continue
            self.objectspace.informations.add(
                variable.path, "examples", tuple(variable.examples)
            )

    def convert_help(self):
        """Convert variable help"""
        for variable in self.get_variables():
            if not hasattr(variable, "help") or not variable.help:
                continue
            self.objectspace.informations.add(variable.path, "help", variable.help)
            del variable.help

    def verify_choices(self):
        for variable in self.get_variables():
            # FIXME
            #            if variable.type is None and variable.choices:
            #                # choice type inference from the `choices` attribute
            #                variable.type = "choice"
            if variable.type != "choice":
                continue
            if variable.default is None:
                continue
            if None in variable.choices and variable.mandatory is None:
                variable.mandatory = False
            if not isinstance(variable.choices, list):
                continue
            choices = variable.choices
            has_calculation = False
            for choice in choices:
                if isinstance(choice, Calculation):
                    has_calculation = True
                    break
            if has_calculation:
                continue

            default = variable.default
            if not isinstance(default, list):
                default = [default]
            for value in default:
                if isinstance(value, Calculation):
                    continue
                if value not in choices:
                    msg = _(
                        'the variable "{0}" has an unvalid default value "{1}" should be in {2}'
                    ).format(
                        variable.path,
                        value,
                        display_list(choices, separator="or", add_quote=True),
                    )
                    raise DictConsistencyError(msg, 26, variable.xmlfiles)
