"""Rougail object model

Silique (https://www.silique.fr)
Copyright (C) 2023-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 warnings import warn
from typing import Optional, Union, get_type_hints, Any, Literal, List, Dict, Iterator
from pydantic import (
    BaseModel,
    StrictBool,
    StrictInt,
    StrictFloat,
    StrictStr,
    ConfigDict,
)
import tiramisu
from ..utils import (
    get_jinja_variable_to_param,
    calc_multi_for_type_variable,
    undefined,
    PROPERTY_ATTRIBUTE,
)
from ..i18n import _
from ..error import (
    DictConsistencyError,
    VariableCalculationDependencyError,
    RougailWarning,
)
from ..tiramisu import CONVERT_OPTION, RENAME_TYPE, display_xmlfiles, convert_boolean

BASETYPE = Union[StrictBool, StrictInt, StrictFloat, StrictStr, None]


def get_convert_option_types():
    for typ, datas in CONVERT_OPTION.items():
        typ_description = datas.get("msg", typ)
        obj = getattr(tiramisu, datas["opttype"])
        initkwargs = datas.get("initkwargs", {})
        if obj == tiramisu.SymLinkOption:
            continue
        if obj == tiramisu.ChoiceOption:
            inst = obj("a", "a", ("a",), **initkwargs)
        else:
            inst = obj("a", "a", **initkwargs)
        extra = getattr(inst, "_extra", {})
        if not extra:
            continue
        params = []
        for key, value in extra.items():
            if key.startswith("_"):
                continue
            if "params" in datas and key in datas["params"]:
                multi = datas["params"][key].get("multi", False)
                description = datas["params"][key]["description"]
                choices = datas["params"][key].get("choices")
            else:
                description = None
                choices = None
                multi = False
            if choices:
                key_type = "choice"
            elif isinstance(value, bool):
                key_type = "boolean"
            elif isinstance(value, str):
                key_type = "string"
            elif isinstance(value, list):
                key_type = "string"
                multi = True
            params.append((key, key_type, description, multi, value, choices))
        yield typ, typ_description, params


class Param(BaseModel):
    key: StrictStr
    namespace: Optional[StrictStr]
    model_config = ConfigDict(extra="forbid")

    def __init__(
        self,
        path,
        attribute,
        family_is_dynamic,
        xmlfiles,
        **kwargs,
    ) -> None:
        super().__init__(**kwargs)

    def to_param(
        self, attribute_name, objectspace, path, version, namespace, xmlfiles
    ) -> dict:
        return self.model_dump()


class AnyParam(Param):
    type: StrictStr
    value: Union[BASETYPE, List[BASETYPE]]


class VariableParam(Param):
    type: StrictStr
    variable: StrictStr
    propertyerror: bool = True
    whole: bool = False
    #    dynamic: bool = True
    optional: bool = False

    def to_param(
        self, attribute_name, objectspace, path, version, namespace, xmlfiles
    ) -> dict:
        param = super().to_param(
            attribute_name, objectspace, path, version, namespace, xmlfiles
        )
        variable, identifier = objectspace.paths.get_with_dynamic(
            param["variable"],
            path,
            version,
            namespace,
            xmlfiles,
        )
        if not variable:
            if not param.get("optional"):
                msg = _(
                    'cannot find variable "{0}" defined in attribute "{1}" for "{2}"'
                ).format(param["variable"], attribute_name, path)
                raise DictConsistencyError(msg, 22, xmlfiles)
            return None
        if isinstance(variable, objectspace.family_objects[-1]["object"]):
            msg = _(
                'the variable "{0}" is in fact a family in attribute "{1}" for "{2}"'
            ).format(variable["name"], attribute_name, path)
            raise DictConsistencyError(msg, 42, xmlfiles)
        if not isinstance(variable, objectspace.variable_objects[0]["object"]):
            msg = _('unknown object "{0}" in attribute "{1}" for "{2}"').format(
                variable, attribute_name, path
            )
            raise DictConsistencyError(msg, 44, xmlfiles)
        param["variable"] = variable
        if identifier:
            param["identifier"] = identifier
        return param


class IdentifierParam(Param):
    type: StrictStr
    identifier: Optional[int] = None

    def __init__(
        self,
        **kwargs,
    ) -> None:
        if not kwargs["family_is_dynamic"]:
            msg = _(
                'identifier parameter for "{0}" in "{1}" cannot be set none dynamic family'
            ).format(kwargs["attribute"], kwargs["path"])
            raise DictConsistencyError(msg, 10, kwargs["xmlfiles"])
        super().__init__(**kwargs)


class InformationParam(Param):
    type: StrictStr
    information: StrictStr
    variable: Optional[StrictStr] = None

    def to_param(
        self, attribute_name, objectspace, path, version, namespace, xmlfiles
    ) -> dict:
        param = super().to_param(
            attribute_name, objectspace, path, version, namespace, xmlfiles
        )
        if not param["variable"]:
            del param["variable"]
            return param
        variable, identifier = objectspace.paths.get_with_dynamic(
            param["variable"],
            path,
            version,
            namespace,
            xmlfiles,
        )
        if not variable:
            msg = _('cannot find variable "{0}" defined in "{1}" for "{2}"').format(
                param["variable"], attribute_name, path
            )
            raise DictConsistencyError(msg, 14, xmlfiles)
        if identifier:
            msg = _(
                'variable "{0}" defined in "{1}" for "{2}" is a dynamic variable'
            ).format(param["variable"], attribute_name, path)
            raise DictConsistencyError(msg, 15, xmlfiles)
        param["variable"] = variable
        return param


class IndexParam(Param):
    type: StrictStr

    def to_param(
        self, attribute_name, objectspace, path, version, namespace, xmlfiles
    ) -> dict:
        if path not in objectspace.followers and (
            attribute_name != "validators" or path not in objectspace.multis
        ):
            msg = _(
                'the variable "{0}" is not a follower, so cannot have index type for param in "{1}"'
            ).format(path, attribute)
            raise DictConsistencyError(msg, 25, xmlfiles)
        return super().to_param(
            attribute_name, objectspace, path, version, namespace, xmlfiles
        )


class NamespaceParam(Param):
    type: StrictStr
    namespace: StrictStr

    def to_param(
        self, attribute_name, objectspace, path, version, namespace, xmlfiles
    ) -> dict:
        namespace = self.namespace
        if namespace:
            namespace = objectspace.paths[namespace].description
        return {
            "type": "any",
            "value": namespace,
            "key": self.key,
        }


PARAM_TYPES = {
    "any": AnyParam,
    "variable": VariableParam,
    "identifier": IdentifierParam,
    "information": InformationParam,
    "index": IndexParam,
    "namespace": NamespaceParam,
}


class Calculation(BaseModel):
    # path: StrictStr
    inside_list: bool
    version: StrictStr
    ori_path: Optional[StrictStr] = None
    default_values: Any = None
    namespace: Optional[StrictStr]
    # FIXME warnings is not available for Properties!
    warnings: Optional[bool] = None
    description: Optional[StrictStr] = None
    xmlfiles: List[StrictStr]

    model_config = ConfigDict(extra="forbid")

    def get_params(self, objectspace, path):
        if self.warnings is not None and self.attribute_name != "validators":
            msg = _(
                '"warnings" are only available with attribute "{self.attribute_name}" for variable "{self.path}"'
            )
            raise DictConsistencyError(msg, 83, xmlfiles)
        if not self.params:
            return {}
        params = {}
        for param_obj in self.params:
            param = param_obj.to_param(
                self.attribute_name,
                objectspace,
                path,
                self.version,
                self.namespace,
                self.xmlfiles,
            )
            if param is None:
                continue
            params[param.pop("key")] = param
        return params


class JinjaCalculation(Calculation):
    attribute_name: Literal[
        "frozen",
        "hidden",
        "mandatory",
        "empty",
        "disabled",
        "default",
        "validators",
        "choices",
        "dynamic",
        "secret_manager",
        "identifier",
    ]
    jinja: StrictStr
    params: Optional[List[Param]] = None
    return_type: BASETYPE = None

    def _jinja_to_function(
        self,
        function,
        return_type,
        multi,
        objectspace,
        path,
        *,
        add_help=False,
        params: Optional[dict] = None,
    ):
        internal_variable = path
        jinja_path = f"{self.attribute_name}_{path}"
        idx = 0
        while jinja_path in objectspace.jinja:
            jinja_path = f"{self.attribute_name}_{path}_{idx}"
            idx += 1
        if self.ori_path is not None:
            path = self.ori_path
        variable = objectspace.paths[path]
        objectspace.jinja[jinja_path] = self.jinja
        if return_type in RENAME_TYPE:
            warning = _(
                'the variable "{0}" has a depreciated return_type "{1}", please use "{2}" instead in {3}'
            )
            warn(
                warning.format(
                    path,
                    return_type,
                    RENAME_TYPE[return_type],
                    display_xmlfiles(self.xmlfiles),
                ),
                DeprecationWarning,
                stacklevel=2,
            )
            return_type = RENAME_TYPE[return_type]
        default = {
            "function": function,
            "params": {
                "__internal_jinja": jinja_path,
                "__internal_type": return_type,
                "__internal_multi": multi,
                "__internal_files": self.xmlfiles,
                "__internal_attribute": self.attribute_name,
                "__internal_variable": internal_variable,
            },
        }
        if self.default_values:
            default["params"]["__default_value"] = self.default_values
        if add_help:
            default["help"] = function + "_help"
        if self.params:
            default["params"] |= self.get_params(objectspace, path)
        if params:
            default["params"] |= params
        if self.warnings:
            default["warnings_only"] = True
        for sub_variable, identifier, true_path in get_jinja_variable_to_param(
            path,
            self.jinja,
            objectspace,
            variable.xmlfiles,
            objectspace.functions,
            self.version,
            self.namespace,
        ):
            if true_path in default["params"]:
                continue
            if isinstance(sub_variable, dict):
                default["params"][true_path] = {
                    "type": "value",
                    "value": sub_variable,
                }
            else:
                default["params"][true_path] = {
                    "type": "variable",
                    "variable": sub_variable,
                }
                if self.version != "1.0":
                    default["params"][true_path]["propertyerror"] = False
                    default["params"][true_path]["optional"] = True
                if identifier:
                    default["params"][true_path]["identifier"] = identifier
        return default

    def to_function(
        self,
        objectspace,
        path,
    ) -> dict:
        if self.attribute_name in ["default", "secret_manager"]:
            if self.ori_path is not None:
                path = self.ori_path
            if self.return_type:
                raise Exception("return_type not allowed!")
            variable = objectspace.paths[path]
            return_type = variable.type
            if self.inside_list:
                multi = False
            elif path in objectspace.followers:
                multi = objectspace.multis[path] == "submulti"
            else:
                multi = path in objectspace.multis
            return self._jinja_to_function(
                "jinja_to_function",
                return_type,
                multi,
                objectspace,
                path,
            )
        elif self.attribute_name == "validators":
            return_type = self.return_type
            if return_type is None:
                return_type = "string"
            if return_type not in ["string", "boolean"]:
                if self.ori_path is not None:
                    path = self.ori_path
                msg = _(
                    'variable "{0}" has a calculating "{1}" with an invalid return_type, should be boolean or string, not "{2}"'
                ).format(path, self.attribute_name, return_type)
                raise DictConsistencyError(msg, 81, self.xmlfiles)
            if return_type == "boolean":
                description = self.description
                if description is None:
                    if self.ori_path is not None:
                        opath = self.ori_path
                    else:
                        opath = path
                    warning = _(
                        'the variable "{0}" has a return_type "{1}", for attribute "{2}" but has not description in {3}'
                    )
                    warn(
                        warning.format(
                            opath,
                            return_type,
                            self.attribute_name,
                            display_xmlfiles(self.xmlfiles),
                        ),
                        RougailWarning,
                    )
                    self.description = _("value is invalid")
            else:
                description = None
            return self._jinja_to_function(
                "valid_with_jinja",
                return_type,
                False,
                objectspace,
                path,
                params={"description": description},
            )
        elif self.attribute_name in PROPERTY_ATTRIBUTE:
            return_type = self.return_type
            if return_type is None:
                return_type = "string"
            if return_type not in ["string", "boolean"]:
                if self.ori_path is not None:
                    path = self.ori_path
                msg = _(
                    'variable "{0}" has a calculating "{1}" with an invalid return_type, should be boolean or string, not "{2}"'
                ).format(path, self.attribute_name, return_type)
                raise DictConsistencyError(msg, 81, self.xmlfiles)
            description = self.description
            return self._jinja_to_function(
                "jinja_to_property",
                return_type,
                False,
                objectspace,
                path,
                add_help=True,
                params={
                    None: [self.attribute_name, description],
                    "when": True,
                    "inverse": False,
                },
            )
        elif self.attribute_name == "choices":
            return_type = self.return_type
            if return_type is None:
                return_type = "string"
            return self._jinja_to_function(
                "jinja_to_function",
                return_type,
                not self.inside_list,
                objectspace,
                path,
            )
        elif self.attribute_name == "dynamic":
            return_type = self.return_type
            if return_type is None:
                return_type = "string"
            return self._jinja_to_function(
                "jinja_to_function",
                return_type,
                True,
                objectspace,
                path,
            )
        raise Exception("hu?")


class _VariableCalculation(Calculation):
    variable: StrictStr
    propertyerror: bool = (True,)
    allow_none: bool = False
    optional: bool = False
    # FIXME identifier is not available for Properties!
    identifier: Optional[Calculation] = None

    def get_variable(
        self,
        objectspace,
        path,
    ) -> "Variable":
        if self.version != "1.0" and objectspace.paths.regexp_relative.search(
            self.variable
        ):
            variable_full_path = objectspace.paths.get_full_path(
                self.variable,
                path,
            )
        elif self.version == "1.0" and "{{ suffix }}" in self.variable:
            variable_full_path = self.variable.replace(
                "{{ suffix }}", "{{ identifier }}"
            )
        else:
            variable_full_path = self.variable
        variable, identifier = objectspace.paths.get_with_dynamic(
            variable_full_path,
            path,
            self.version,
            self.namespace,
            self.xmlfiles,
        )
        if variable and not isinstance(
            variable, objectspace.variable_objects[0]["object"]
        ):
            if isinstance(variable, objectspace.family):
                msg = _(
                    'a variable "{0}" is needs in attribute "{1}" for "{2}" but it\'s a family'
                ).format(variable_full_path, self.attribute_name, path)
                raise DictConsistencyError(msg, 47, self.xmlfiles)
            else:
                msg = _('unknown object "{0}" in attribute "{1}" for "{2}"').format(
                    variable, self.attribute_name, path
                )
                raise DictConsistencyError(msg, 48, self.xmlfiles)
        return variable_full_path, variable, identifier

    def get_params(
        self,
        objectspace,
        path,
        variable_in_calculation_path: str,
        variable_in_calculation: "Variable",
        variable_in_calculation_identifier: Optional[str],
        key: str = None,
    ):
        identifier_is_a_calculation = False
        if self.identifier:
            if variable_in_calculation_identifier:
                msg = _(
                    'variable "{0}" has an attribute "{1}" with an identifier "{2}" but the path has also the identifier "{3}"'
                ).format(
                    path,
                    self.attribute_name,
                    self.variable,
                    self.identifier,
                    variable_in_calculation_identifier,
                )
                raise DictConsistencyError(msg, 89, self.xmlfiles)
            variable_in_calculation_identifier = self.identifier
            identifier_is_a_calculation = True
        if not variable_in_calculation:
            if not objectspace.force_optional:
                msg = _(
                    'variable "{0}" has an attribute "{1}" calculated with the unknown variable "{2}"'
                ).format(path, self.attribute_name, self.variable)
                raise DictConsistencyError(msg, 88, self.xmlfiles)
            return {None: [["example"]]}
        param = {
            "type": "variable",
            "variable": variable_in_calculation,
            "propertyerror": self.propertyerror,
        }
        if isinstance(self, VariableCalculation) and self.optional:
            param["optional"] = self.optional
        if variable_in_calculation_identifier:
            param["identifier"] = variable_in_calculation_identifier
        if key:
            params = {key: param}
        else:
            params = {None: [param]}
        if self.default_values:
            params["__default_value"] = self.default_values
        if self.allow_none:
            params["allow_none"] = True
        self.check_multi(
            objectspace,
            path,
            variable_in_calculation_path,
            variable_in_calculation,
            identifier_is_a_calculation,
        )
        if path in objectspace.followers:
            multi = objectspace.multis[path] == "submulti"
        else:
            multi = path in objectspace.multis
        if multi and not self.inside_list:
            params["__internal_multi"] = True
        return params

    def check_multi(
        self,
        objectspace,
        path,
        variable_in_calculation_path,
        variable_in_calculation,
        identifier_is_a_calculation,
    ):
        local_variable = objectspace.paths[path]
        local_variable_multi, variable_in_calculation_multi = (
            calc_multi_for_type_variable(
                local_variable,
                variable_in_calculation_path,
                variable_in_calculation,
                identifier_is_a_calculation,
                objectspace,
            )
        )
        if self.attribute_name == "default":
            if variable_in_calculation_multi == "submulti":
                if objectspace.paths.is_dynamic(variable_in_calculation.path):
                    msg = _(
                        'the variable "{0}" has an invalid "{1}" the variable "{2}" is in a sub dynamic option'
                    ).format(
                        local_variable.path,
                        self.attribute_name,
                        variable_in_calculation.path,
                    )
                    raise DictConsistencyError(msg, 69, self.xmlfiles)
                else:
                    msg = _(
                        'the leader "{0}" has an invalid "{1}" the follower "{2}" is a multi'
                    ).format(
                        local_variable.path,
                        self.attribute_name,
                        variable_in_calculation.path,
                    )
                    raise DictConsistencyError(msg, 74, self.xmlfiles)
            if not self.inside_list:
                if local_variable_multi != variable_in_calculation_multi:
                    if local_variable_multi:
                        self.check_variable_in_calculation_multi(
                            local_variable.path,
                            variable_in_calculation_path,
                            variable_in_calculation_multi,
                        )
                    self.check_variable_in_calculation_not_multi(
                        local_variable.path,
                        variable_in_calculation_path,
                        variable_in_calculation_multi,
                    )
            else:
                self.check_variable_in_calculation_in_list_not_multi(
                    local_variable.path,
                    variable_in_calculation_path,
                    variable_in_calculation_multi,
                )
        elif self.attribute_name in ["choices", "dynamic"]:
            # calculated variable must be a multi
            if not self.inside_list:
                self.check_variable_in_calculation_multi(
                    local_variable.path,
                    variable_in_calculation_path,
                    variable_in_calculation_multi,
                )
            else:
                self.check_variable_in_calculation_in_list_not_multi(
                    local_variable.path,
                    variable_in_calculation_path,
                    variable_in_calculation_multi,
                )
        elif variable_in_calculation_multi is True:
            msg = _(
                'the variable "{0}" has an invalid attribute "{1}", the variable "{2}" must not be multi'
            ).format(
                local_variable.path, self.attribute_name, variable_in_calculation_path
            )
            raise DictConsistencyError(msg, 23, self.xmlfiles)

    def check_variable_in_calculation_multi(
        self,
        local_variable_path,
        variable_in_calculation_path,
        variable_in_calculation_multi,
    ):
        if variable_in_calculation_multi is False:
            msg = _(
                'the variable "{0}" has an invalid attribute "{1}", the variable must not be a multi or the variable "{2}" must be multi'
            ).format(
                local_variable_path, self.attribute_name, variable_in_calculation_path
            )
            raise DictConsistencyError(msg, 20, self.xmlfiles)

    def check_variable_in_calculation_not_multi(
        self,
        local_variable_path,
        variable_in_calculation_path,
        variable_in_calculation_multi,
    ):
        if variable_in_calculation_multi is True:
            msg = _(
                'the variable "{0}" has an invalid attribute "{1}", the variable must be a multi or the variable "{2}" must not be multi'
            ).format(
                local_variable_path, self.attribute_name, variable_in_calculation_path
            )
            raise DictConsistencyError(msg, 21, self.xmlfiles)

    def check_variable_in_calculation_in_list_not_multi(
        self,
        local_variable_path,
        variable_in_calculation_path,
        variable_in_calculation_multi,
    ):
        if variable_in_calculation_multi is True:
            msg = _(
                'the variable "{0}" has an invalid attribute "{1}", the variable "{2}" is multi but is inside a list'
            ).format(
                local_variable_path, self.attribute_name, variable_in_calculation_path
            )
            raise DictConsistencyError(msg, 18, self.xmlfiles)

    def get_default_value_optional(self, objectspace, path, default):
        if self.attribute_name == "default":
            if self.inside_list:
                expected_multiple_value = False
            elif path in objectspace.followers:
                expected_multiple_value = objectspace.multis[path] == "submulti"
            else:
                expected_multiple_value = path in objectspace.multis
        elif self.attribute_name in PROPERTY_ATTRIBUTE:
            expected_multiple_value = False
        else:
            expected_multiple_value = True
        value_is_multi = isinstance(default, list)
        if expected_multiple_value != value_is_multi:
            if self.attribute_name != "default" or expected_multiple_value:
                msg = _(
                    'the variable "{0}" is waiting for a list as "{1}" but the attribute "default" is not a list ("{2}")'
                )
            else:
                msg = _(
                    'the variable "{0}" is not waiting for a list as "{1}" but the attribute "default" is a list ("{2}")'
                )
            msg = msg.format(path, self.attribute_name, default)
            raise DictConsistencyError(msg, 77, self.xmlfiles)
        return default


class VariableCalculation(_VariableCalculation):
    attribute_name: Literal["default", "choices", "dynamic", "identifier"]
    default: Any = undefined

    def to_function(
        self,
        objectspace,
        path,
    ) -> dict:
        if self.ori_path is not None:
            path = self.ori_path
        if (
            self.attribute_name != "default"
            and self.optional
            and self.default is undefined
        ):
            msg = _(
                '"{0}" attribut shall not have an "optional" attribute without the "default" attribute for variable "{1}"'
            ).format(self.attribute_name, self.variable)
            raise DictConsistencyError(msg, 33, self.xmlfiles)
        (
            variable_in_calculation_path,
            variable_in_calculation,
            variable_in_calculation_identifier,
        ) = self.get_variable(objectspace, path)
        if not variable_in_calculation and (
            self.optional or objectspace.force_optional
        ):
            if self.default is not undefined:
                return self.get_default_value_optional(objectspace, path, self.default)
            if self.default_values is not None:
                return self.default_values
            raise VariableCalculationDependencyError()
        if variable_in_calculation and self.attribute_name == "default":
            local_variable = objectspace.paths[path]
            if CONVERT_OPTION.get(local_variable.type, {}).get(
                "func", str
            ) != CONVERT_OPTION.get(variable_in_calculation.type, {}).get("func", str):
                msg = _(
                    'variable "{0}" has a default value calculated with "{1}" which has incompatible type'
                ).format(path, self.variable)
                raise DictConsistencyError(msg, 67, self.xmlfiles)
        params = self.get_params(
            objectspace,
            path,
            variable_in_calculation_path,
            variable_in_calculation,
            variable_in_calculation_identifier,
        )
        return {
            "function": "calc_value",
            "params": params,
        }


class VariablePropertyCalculation(_VariableCalculation):
    attribute_name: Literal[*PROPERTY_ATTRIBUTE]
    propertyerror: Union[Literal["transitive"], bool] = True
    when: Any = undefined
    when_not: Any = undefined
    default: bool = False

    def to_function(
        self,
        objectspace,
        path,
    ) -> dict:
        if self.ori_path is not None:
            path = self.ori_path
        (
            variable_in_calculation_path,
            variable_in_calculation,
            variable_in_calculation_identifier,
        ) = self.get_variable(objectspace, path)
        if (
            #            self.default is not undefined and
            not variable_in_calculation
            and (self.optional or objectspace.force_optional)
        ):
            if self.default is undefined:
                default = False
            else:
                default = self.default
                if not isinstance(default, bool):
                    msg = _(
                        'the variable "{0}" is waiting for a boolean as "{1}" but the attribute "default" is not a boolean ("{2}")'
                    )
                    msg = msg.format(path, self.attribute_name, default)
                    raise DictConsistencyError(msg, 79, self.xmlfiles)
            return self.get_default_value_optional(objectspace, path, default)
        params = self.get_params(
            objectspace,
            path,
            variable_in_calculation_path,
            variable_in_calculation,
            variable_in_calculation_identifier,
            key="value",
        )
        params["prop"] = self.attribute_name
        func = "variable_to_property"
        if objectspace.force_optional and (
            not params["value"] or "variable" not in params["value"]
        ):
            params = {"value": None, "when": None, "inverse": False}
        else:
            variable = params["value"]["variable"]
            if self.when is not undefined:
                if self.version == "1.0":
                    msg = _(
                        '"when" is not allowed in format version 1.0 for attribute "{0}" for variable "{1}"'
                    ).format(self.attribute_name, path)
                    raise DictConsistencyError(msg, 103, variable.xmlfiles)
                if self.when_not is not undefined:
                    msg = _(
                        'the variable "{0}" has an invalid attribute "{1}", "when" and "when_not" cannot set together'
                    ).format(path, self.attribute_name)
                    raise DictConsistencyError(msg, 31, variable.xmlfiles)
                params["when"] = self.when
                params["inverse"] = False
            elif self.when_not is not undefined:
                if self.version == "1.0":
                    msg = _(
                        '"when_not" is not allowed in format version 1.0 for attribute "{0}" for variable "{1}"'
                    ).format(self.attribute_name, path)
                    raise DictConsistencyError(msg, 104, variable.xmlfiles)
                params["when"] = self.when_not
                params["inverse"] = True
            elif self.propertyerror != "transitive":
                if variable.multi:
                    params["when"] = []
                else:
                    if variable.type != "boolean":
                        msg = _(
                            '"when" or "when_not" is mandatory for the not boolean variable "{0}" in attribute "{1}"'
                        ).format(path, self.attribute_name)
                        raise DictConsistencyError(msg, 106, variable.xmlfiles)
                    params["when"] = True
                params["inverse"] = False
            else:
                func = "variable_to_property_transitive"
        return {
            "function": func,
            "params": params,
            "help": func,
        }


class InformationCalculation(Calculation):
    attribute_name: Literal["default", "choice", "dynamic", "identifier"]
    information: StrictStr
    variable: Optional[StrictStr]

    def to_function(
        self,
        objectspace,
        path,
    ) -> dict:
        if self.ori_path is not None:
            path = self.ori_path
        params = {
            None: [
                {
                    "type": "information",
                    "information": self.information,
                }
            ]
        }
        if self.variable:
            variable, identifier = objectspace.paths.get_with_dynamic(
                self.variable,
                path,
                self.version,
                self.namespace,
                self.xmlfiles,
            )
            if variable is None:
                if not objectspace.force_optional:
                    msg = _(
                        'cannot find variable "{0}" for the information "{1}" when calculating "{2}"'
                    ).format(self.variable, self.information, self.attribute_name)
                    raise DictConsistencyError(msg, 40, self.xmlfiles)
            if identifier is not None:
                msg = _(
                    'identifier not allowed for the information "{0}" when calculating "{1}"'
                ).format(self.information, self.attribute_name)
                raise DictConsistencyError(msg, 41, self.xmlfiles)
            if variable:
                params[None][0]["variable"] = variable
        if self.default_values:
            params["__default_value"] = self.default_values
        return {
            "function": "calc_value",
            "params": params,
        }


class _IdentifierCalculation(Calculation):
    identifier: Optional[int] = None

    def get_identifier(self) -> dict:
        identifier = {"type": "identifier"}
        if self.identifier is not None:
            identifier["identifier"] = self.identifier
        return identifier


class IdentifierCalculation(_IdentifierCalculation):
    attribute_name: Literal["default", "choice", "dynamic", "identifier"]

    def to_function(
        self,
        objectspace,
        path,
    ) -> dict:
        identifier = {"type": "identifier"}
        if self.identifier is not None:
            identifier["identifier"] = self.identifier
        return {
            "function": "calc_value",
            "params": {None: [self.get_identifier()]},
        }


class IdentifierPropertyCalculation(_IdentifierCalculation):
    attribute_name: Literal[*PROPERTY_ATTRIBUTE]
    when: Any = undefined
    when_not: Any = undefined

    def to_function(
        self,
        objectspace,
        path,
    ) -> dict:
        if self.version == "1.0":
            msg = _(
                '"when" is not allowed in format version 1.0 for attribute "{0}"'
            ).format(self.attribute_name)
            raise DictConsistencyError(msg, 105, variable.xmlfiles)
        if self.when is not undefined:
            if self.when_not is not undefined:
                msg = _(
                    'the identifier has an invalid attribute "{0}", "when" and "when_not" cannot set together'
                ).format(self.attribute_name)
                raise DictConsistencyError(msg, 35, variable.xmlfiles)
            when = self.when
            inverse = False
        elif self.when_not is not undefined:
            when = self.when_not
            inverse = True
        else:
            msg = _(
                'the identifier has an invalid attribute "{0}", "when" and "when_not" cannot set together'
            ).format(self.attribute_name)
            raise DictConsistencyError
        params = {
            "prop": self.attribute_name,
            "value": self.get_identifier(),
            "when": when,
            "inverse": inverse,
        }
        return {
            "function": "variable_to_property",
            "params": params,
            "help": "variable_to_property",
        }


class IndexCalculation(Calculation):
    attribute_name: Literal["default", "choice", "dynamic", "identifier"]

    def to_function(
        self,
        objectspace,
        path,
    ) -> dict:
        if path not in objectspace.followers:
            msg = _(
                'the variable "{0}" is not a follower, so cannot have index type for "{1}"'
            ).format(path, self.attribute_name)
            raise DictConsistencyError(msg, 60, self.xmlfiles)
        return {
            "function": "calc_value",
            "params": {None: [{"type": "index"}]},
        }


class NamespaceCalculation(Calculation):
    attribute_name: Literal["default", "secret_manager"]

    def to_function(
        self,
        objectspace,
        path,
    ) -> dict:
        namespace = self.namespace
        if namespace:
            namespace = objectspace.paths[namespace].description
        return namespace


CALCULATION_TYPES = {
    "jinja": JinjaCalculation,
    "information": InformationCalculation,
    "variable": VariableCalculation,
    "identifier": IdentifierCalculation,
    # FOR VERSION 1.0
    "suffix": IdentifierCalculation,
    "index": IndexCalculation,
    "namespace": NamespaceCalculation,
}
CALCULATION_PROPERTY_TYPES = {
    "jinja": JinjaCalculation,
    "information": InformationCalculation,
    "variable": VariablePropertyCalculation,
    "identifier": IdentifierPropertyCalculation,
    "index": IndexCalculation,
}
BASETYPE_CALC = Union[StrictBool, StrictInt, StrictFloat, StrictStr, Calculation, None]
SECRET_BASETYPE_CALC = Union[StrictStr, JinjaCalculation]


class Family(BaseModel):
    name: StrictStr
    # informations
    description: Optional[StrictStr] = None
    help: Optional[StrictStr] = None
    mode: Optional[StrictStr] = None
    # validation
    type: Literal["family", "leadership", "sequence"] = "family"
    # properties
    hidden: Union[bool, Calculation] = False
    disabled: Union[bool, Calculation] = False
    tags: Optional[List[StrictStr]] = None
    # others
    namespace: Optional[StrictStr]
    path: StrictStr
    version: StrictStr
    xmlfiles: List[StrictStr] = []

    model_config = ConfigDict(extra="forbid", arbitrary_types_allowed=True)


class Dynamic(BaseModel):
    type: Literal["dynamic"] = "dynamic"
    #    # None only for format 1.0
    #    variable: StrictStr = None
    dynamic: Union[List[Union[StrictStr, Calculation]], Calculation]


class Variable(BaseModel):
    name: StrictStr
    # user informations
    description: Optional[StrictStr] = None
    help: Optional[StrictStr] = None
    mode: Optional[StrictStr] = None
    tags: Optional[List[StrictStr]] = None
    examples: Optional[List[BASETYPE]] = None
    test: Optional[List[BASETYPE]] = None
    # validations
    ## type will be set dynamically in `annotator/value.py`, default is None
    type: StrictStr = None
    params: Optional[List[Param]] = None
    multi: Optional[bool] = None
    validators: Optional[List[Calculation]] = None
    # FIXME: warnings markes sense? if so, please document it
    warnings: bool = False
    # value
    default: Union[List[BASETYPE_CALC], BASETYPE_CALC] = None
    secret_manager: Optional[JinjaCalculation] = None
    # properties
    auto_save: bool = False
    mandatory: Union[None, bool, Calculation] = None
    # FIXME: terme empty is not appropriate
    empty: Union[None, bool, Calculation] = True
    unique: Optional[bool] = None
    hidden: Union[bool, Calculation] = False
    disabled: Union[bool, Calculation] = False
    frozen: Union[bool, Calculation] = False
    # others
    path: StrictStr
    namespace: Optional[StrictStr]
    version: StrictStr
    xmlfiles: List[StrictStr] = []

    model_config = ConfigDict(extra="forbid", arbitrary_types_allowed=True)


class Choices(BaseModel):
    type: Literal["choice"] = "choice"
    choices: Optional[Union[List[BASETYPE_CALC], Calculation]] = None


class Regexp(BaseModel):
    type: Literal["regexp"] = "regexp"
    regexp: Optional[StrictStr] = None


class SymLink(BaseModel):
    type: Literal["symlink"] = "symlink"
    name: StrictStr
    path: StrictStr
    opt: Variable
    namespace: Optional[StrictStr]
    version: StrictStr
    xmlfiles: List[StrictStr] = []

    model_config = ConfigDict(extra="forbid")
