"""Takes a bunch of Rougail YAML dispatched in differents folders
as an input and outputs a Tiramisu's file.

Silique (https://www.silique.fr)
Copyright (C) 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/>.
"""

import logging
from warnings import warn
from pydantic import ValidationError
from typing import (
    Any,
    Iterator,
    Literal,
    Optional,
)
from tiramisu.error import display_list
from .object_model import (
    CALCULATION_TYPES,
    CALCULATION_PROPERTY_TYPES,
    PROPERTY_ATTRIBUTE,
    PARAM_TYPES,
    AnyParam,
)
from ..i18n import _
from ..error import DictConsistencyError


class CollectFamily:
    def family_collect(self) -> None:
        if self.parameters is None:
            self.parameters = {}
        if self.user_type == "dynamic":
            # FIXME code here only for support 1.0 dynamic flavor
            # DictConsistencyError should be place in annotation
            if "{{ identifier }}" not in self.name:
                if "{{ suffix }}" in self.name:
                    self.name = self.name.replace("{{ suffix }}", "{{ identifier }}")
                    self.path = self.path.replace("{{ suffix }}", "{{ identifier }}")
                elif "variable" in self.parameters:
                    self.name += "{{ identifier }}"
                    self.path += "{{ identifier }}"
                elif self.raises:
                    msg = f'dynamic family name must have "{{{{ identifier }}}}" in his name for "{self.path}"'
                    raise DictConsistencyError(msg, 13, self.sources)
            if self.version == "1.0":
                if self.raises and "variable" not in self.parameters:
                    raise DictConsistencyError(
                        f'dynamic family must have "variable" attribute for "{self.path}"',
                        101,
                        self.sources,
                    )
                if self.raises and "dynamic" in self.parameters:
                    raise DictConsistencyError(
                        'variable and dynamic cannot be set together in the dynamic family "{self.path}"',
                        100,
                        self.sources,
                    )
                self.parameters["dynamic"] = {
                    "type": "variable",
                    "variable": self.parameters["variable"],
                    "propertyerror": False,
                    "allow_none": True,
                }
                del self.parameters["variable"]
            if "dynamic" not in self.parameters and "variable" in self.parameters:
                self.parameters["dynamic"] = {
                    "type": "variable",
                    "variable": self.parameters["variable"],
                    "propertyerror": False,
                    "allow_none": True,
                }
                del self.parameters["variable"]
                if self.version != "1.0":
                    warning = f'"variable" attribute in dynamic family "{self.path}" is depreciated in {display_list(self.sources)}'
                    warn(
                        warning,
                        DeprecationWarning,
                        stacklevel=2,
                    )
            self.object = self.objectspace.family_objects[0]
        else:
            self.object = self.objectspace.family_objects[-1]
        self.split_param_family_children()

    def split_param_family_children(
        self,
    ) -> None:
        """Family declaration mix variable object and family attributes
        Here we separate attribute (which is the correct self.parameters content and variable declaration
        """
        new_parameters = {}
        children = self.parameters
        # when an attribute starts with "_", the variable name without this "_" is a variable
        sub_variables = [
            key[1:]
            for key in self.parameters
            if key.startswith("_") and key[1:] in self.parameters
        ]
        # and known variables
        if self.test_exists and self.path in self.objectspace.paths:
            sub_variables.extend(
                [p.rsplit(".", 1)[-1] for p in self.objectspace.parents[self.path]]
            )
        attributes = self.object["attrs"]
        attributes_types = self.object["attributes_types"]
        if self.types:
            types_children = list(self.types.children)
        else:
            types_children = []
        for key, value in list(children.items()):
            if self.raises and not isinstance(key, str):
                raise DictConsistencyError(
                    f'the key "{key}" is not in string format for family {self.path}',
                    103,
                    self.sources,
                )
            if key.startswith("_"):
                # if key starts with _, it's an attribute
                if key == "_type" and self.types:
                    children.pop(key)
                else:
                    new_parameters[key[1:]] = children.pop(key)
            elif (
                key in types_children
                or key not in attributes
                or key in sub_variables
                or not self.parameter_is_an_attributes(key, value, attributes_types)
            ):
                if key == "type" and self.types and key not in types_children:
                    children.pop(key)
            else:
                new_parameters[key] = children.pop(key)
        if self.version != "1.0" and not new_parameters and self.comment:
            new_parameters["description"] = self.comment
        self.children = children
        self.parameters = new_parameters


class CollectVariable:
    def variable_collect(self) -> None:
        obj = self.parameters
        if self.version == "1.0" or isinstance(obj, dict):
            if obj is None:
                # only with self.version to 1.0
                obj = {}
                self.parameters = obj
            self.parse_type_params()
            self.parse_secret_manager()
            if self.raises and not self.check_no_extra_keys:
                extra_attrs = set(obj) - self.object["attrs"]
                if extra_attrs:
                    raise DictConsistencyError(
                        f'"{self.path}" is not a valid variable, there are additional '
                        f'attributes: "{", ".join(extra_attrs)}"',
                        65,
                        self.sources,
                    )
        else:
            self.parameters = {"default": obj}
            if self.comment:
                self.parameters["description"] = self.comment

    def parse_type_params(self):
        """Parse variable params"""
        if "params" not in self.parameters:
            return
        params = self.parameters["params"]
        if isinstance(params, list):
            # the conversion has already be done (in types purpose)
            for param in params:
                if self.raises and not isinstance(param, AnyParam):
                    raise DictConsistencyError(
                        _("params must be a dict for {0}").format(self.path),
                        55,
                        self.sources,
                    )
            return
        elif self.raises and not isinstance(params, dict):
            raise DictConsistencyError(
                _("params must be a dict for {0}").format(self.path),
                55,
                self.sources,
            )
        new_params = []
        namespace = self.objectspace.namespace
        for key, val in params.items():
            try:
                new_params.append(
                    AnyParam(
                        key=key,
                        value=val,
                        type="any",
                        path=None,
                        attribute=None,
                        family_is_dynamic=None,
                        namespace=namespace,
                        xmlfiles=self.sources,
                    )
                )
            except ValidationError as err:
                if self.raises:
                    raise DictConsistencyError(
                        _('"{0}" has an invalid "params" for {1}: {2}').format(
                            key, self.path, err
                        ),
                        54,
                        self.sources,
                    ) from err
        self.parameters["params"] = new_params

    def parse_secret_manager(self):
        """Parse variable secret_manager"""
        if "secret_manager" not in self.parameters:
            return
        secret_manager = {
            "type": "jinja",
            "jinja": self.objectspace.secret_pattern,
            "params": self.parameters["secret_manager"],
        }
        self.set_calculation(
            "secret_manager",
            secret_manager,
        )


class CollectType:
    """Determine the option type
    1/ self.option_type must be "variable" or "family" option type
    needs to determinate if the option is a variable or a family
    an option is a family if:
    - user specified a know family type: family, sequence (user must set it explicitly) or dynamic)
    - has a family custom type
    - has "dynamic" attribute (a variable could not has dynamic attribut)
    - it's already a family (so we are certainly in redefine purpose
    otherwise it's a variable
    2/ self.user_type must be a more specilized variable or family type defined by the user
    """

    def set_option_types(self):
        if isinstance(self.parameters, dict):
            self.set_user_type()
            self.set_dynamic_user_type()
        else:
            self.short_hand_variable()
        self.set_user_redefined()
        self.variable_in_sequence()
        self.family_or_variable()
        if self.user_type:
            logging.info(
                "family_or_variable: %s is a %s (%s)",
                self.path,
                self.option_type,
                self.user_type,
            )
        else:
            logging.info("family_or_variable: %s is a %s", self.path, self.option_type)

    def set_user_type(
        self,
    ) -> None:
        """Check 'type' attributes and determine the option type"""
        for type_name in ["_type", "type"]:
            if type_name not in self.parameters or not isinstance(
                self.parameters[type_name], str
            ):
                continue
            self.user_type = self.parameters[type_name]
            if self.user_type in self.objectspace.custom_family_types:
                self.set_custom_type(
                    self.objectspace.custom_family_types[self.user_type]
                )
                if self.user_type is None:
                    self.object = self.objectspace.family_objects[-1]
                else:
                    self.set_object_user_type_family()
                break
            elif self.user_type in self.objectspace.custom_variable_types:
                self.set_custom_type(
                    self.objectspace.custom_variable_types[self.user_type]
                )
                if self.user_type is None:
                    self.object = self.objectspace.variable_objects[0]
                else:
                    self.set_object_user_type_variable()
                break
            self.object = None
            self.set_object_user_type_family()
            if not self.object:
                self.set_object_user_type_variable()
            if self.raises and not self.object:
                msg = _('cannot determine the type of the {0} "{1}"').format(
                    self.user_type, self.path
                )
                raise DictConsistencyError(msg, 43, self.sources)
            break

    def set_custom_type(self, custom: dict, from_parent: bool = False) -> None:
        if self.raises and self.test_exists and self.path in self.objectspace.paths:
            msg = f'cannot redefine "{self.path}" object to a custom type'
            raise DictConsistencyError(msg, 64, self.sources)
        self.types = custom
        self.types.name = self.name
        self.types.path = self.path
        self.types.namespace = self.namespace
        self.types.help = None
        self.types.tags = []
        if not from_parent:
            self.types.parameters["help"] = None
            self.types.parameters["tags"] = []
        self.option_type = self.types.option_type
        self.user_type = self.types.user_type
        self.sources = self.types.sources + self.sources
        self.object = self.types.object
        if (
            not from_parent
            and self.option_type == "variable"
            and "type" in self.parameters
        ):
            self.parameters.pop("type")

    def set_object_user_type_family(self):
        for obj in self.objectspace.family_objects:
            if self.user_type in obj["types"]:
                self.option_type = "family"
                self.object = obj
                return

    def set_object_user_type_variable(self):
        for obj in self.objectspace.variable_objects:
            if self.user_type in obj["types"]:
                self.option_type = "variable"
                self.object = obj
                return

    def set_dynamic_user_type(self) -> None:
        if self.user_type:
            return
        for type_name in ["_dynamic", "dynamic"]:
            if type_name in self.parameters and isinstance(
                self.parameters[type_name], (list, dict)
            ):
                self.user_type = "dynamic"
                self.option_type = "family"
                break

    def set_user_redefined(self) -> None:
        if not self.test_exists or self.path not in self.objectspace.paths:
            return
        # it's already a variable or a family
        old_option_type = self.option_type if self.user_type else None
        self.option_type = (
            "family" if self.path in self.objectspace.families else "variable"
        )
        if self.raises and old_option_type and self.option_type != old_option_type:
            msg = f'the {old_option_type} "{self.path}" is redefine as a {self.option_type}, which is not allowed'
            raise DictConsistencyError(msg, 11, self.sources)
        if self.user_type:
            return
        self.user_type = self.objectspace.paths[self.path].type
        if self.option_type == "family":
            for obj in self.objectspace.family_objects:
                if self.user_type in obj["types"]:
                    self.object = obj
                    break
            else:
                self.object = self.objectspace.variable_objects[0]
        else:
            for obj in self.objectspace.variable_objects:
                if self.user_type in obj["types"]:
                    self.object = obj
                    break
            else:
                self.object = self.objectspace.variable_objects[-1]

    def short_hand_variable(self):
        if self.option_type:
            return
        # it's something like: "my_variable:" or "my_variable: True"
        self.option_type = "variable"
        self.object = self.objectspace.variable_objects[0]

    def variable_in_sequence(self):
        # in a sequence there have only variables
        if not self.option_type and self.family_is_sequence:
            self.option_type = "variable"
            if self.raises and not self.find_variable_object():
                msg = f'"{self.path}" seems to be a family, which is not allowed in a family'
                raise DictConsistencyError(msg, 17, self.sources)

    def family_or_variable(
        self,
    ) -> Literal["variable", "family"]:
        """check if all attributes are attributes of a variable"""
        if self.option_type:
            return
        if not self.find_variable_object():
            self.option_type = "family"

    def find_variable_object(self) -> bool:
        attrs = set(self.parameters)
        for variable in self.objectspace.variable_objects:
            if not attrs - variable["attrs"] and self.check_variable_parameters(
                variable
            ):
                self.option_type = "variable"
                self.check_no_extra_keys = True
                self.object = variable
                return True
        return False

    def check_variable_parameters(self, variable):
        attributes_types = variable["attributes_types"]
        attrs = variable["attrs"]
        for key, value in list(self.parameters.items()):
            if key not in attrs:
                return False
            if not self.parameter_is_an_attributes(key, value, attributes_types):
                return False
        return True

    def parameter_is_an_attributes(
        self, key: str, value: Any, attributes_types: dict
    ) -> bool:
        if value is None:
            return True
        if (
            key in attributes_types["literals"]
            and value in attributes_types["literals"][key]
        ):
            return True
        for k, typ in [
            ("strings", str),
            ("booleans", bool),
            ("integers", int),
            ("floats", float),
        ]:
            if key in attributes_types[k] and isinstance(value, typ):
                return True
        if isinstance(value, dict):
            if key in attributes_types["calculation"]:
                return self.is_calculation(
                    key, value, attributes_types=attributes_types
                )
            if key in attributes_types["params"]:
                return True
        if isinstance(value, list):
            current_value = value.copy()
            for k, typ in [
                ("strings", str),
                ("booleans", bool),
                ("integers", int),
                ("floats", float),
            ]:
                if key in attributes_types["lists"][k]:
                    for idx, val in reversed(list(enumerate(current_value))):
                        if val is not None and not isinstance(val, typ):
                            continue
                        del current_value[idx]
            if not current_value:
                return True
            if key in attributes_types["lists"]["calculation"]:
                for val in current_value:
                    if isinstance(val, dict) and not self.is_calculation(
                        key, val, inside_list=True, attributes_types=attributes_types
                    ):
                        return False
                return True
        return False


class Collect(CollectType, CollectFamily, CollectVariable):
    def __init__(
        self,
        objectspace: "ParserVariable",
        name: str,
        subpath: Optional[str],
        parameters: dict,
        comment: Optional[str],
        parent_option: Optional["Collect"],
        *,
        raises: bool = True,
        test_exists: bool = True,
    ) -> None:
        self.sources_types = None
        self.types = None
        self.name = name
        if not subpath:
            path = name
        else:
            path = f"{subpath}.{name}"
        self.raises = raises
        self.test_exists = test_exists
        if self.raises and name.startswith("_"):
            msg = f'the variable or family "{self.path}" is incorrect, it must not starts with "_" character'
            raise DictConsistencyError(msg, 16, obj["sources"])
        self.path = path
        self.subpath = subpath
        self.parameters = parameters
        self.comment = comment
        self.objectspace = objectspace
        self.namespace = objectspace.namespace
        self.sources = objectspace.sources.copy()
        self.user_type = None
        self.option_type = None
        if (
            parent_option is not None
            and parent_option.types is not None
            and self.name in parent_option.types.children
        ):
            self.set_custom_type(
                parent_option.types.children[self.name], from_parent=True
            )
        self.check_no_extra_keys = False
        if self.test_exists and path in objectspace.paths:
            for source in reversed(self.objectspace.paths[path].xmlfiles):
                self.sources.insert(0, source)
        if parent_option:
            self.family_is_sequence = parent_option.user_type in [
                "leadership",
                "sequence",
            ]
            self.family_is_dynamic = parent_option.family_is_dynamic
            self.parent_dynamic = parent_option.parent_dynamic
        else:
            self.family_is_sequence = False
            self.family_is_dynamic = False
            self.parent_dynamic = None
        self.version = self.objectspace.version
        self.set_option_types()
        if self.user_type == "dynamic":
            self.family_is_dynamic = True
            self.parent_dynamic = subpath
        if self.option_type == "family":
            self.family_collect()
        else:
            self.variable_collect()
        self.convert_calculated_parameters()
        self.exists = self.is_exists()
        self.redefine = self.is_redefine()

    def convert_calculated_parameters(
        self,
    ):
        """Parse option attributes and convert calculation"""
        obj = self.parameters
        for key, value in obj.items():
            if self.is_calculation(
                key,
                value,
            ):
                try:
                    self.set_calculation(
                        key,
                        value,
                    )
                except ValidationError as err:
                    if self.raises:
                        raise DictConsistencyError(
                            _('the {0} "{1}" has an invalid "{2}": {3}').format(
                                self.option_type, self.path, key, err
                            ),
                            84,
                            self.sources,
                        ) from err
                continue
            if not isinstance(value, list):
                continue
            for idx, val in enumerate(value):
                if not self.is_calculation(
                    key,
                    val,
                    inside_list=True,
                ):
                    continue
                try:
                    self.set_calculation(
                        key,
                        val,
                        inside_list=True,
                        index=idx,
                    )
                except ValidationError as err:
                    raise Exception(
                        f'the {option.type} "{option.path}" in "{display_list(option.sources)}" has an invalid "{key}" '
                        f"at index {idx}: {err}"
                    ) from err

    def is_dict(
        self, key: str, value: Any, *, attributes_types: Optional[dict] = None
    ) -> bool:
        """it's a dict, so it's a new variables!"""
        return isinstance(value, dict) and not self.is_calculation(
            key, value, attributes_types=attributes_types
        )

    def is_calculation(
        self,
        attribute: str,
        value: dict,
        *,
        attributes_types: Optional[dict] = None,
        inside_list: bool = False,
    ):
        """Check if it's a calculation"""
        if not isinstance(value, dict):
            return False
        if attributes_types is None:
            attributes_types = self.object["attributes_types"]
        if inside_list:
            attributes_types = attributes_types["lists"]["calculation"]
        else:
            attributes_types = attributes_types["calculation"]
        if attribute not in attributes_types:
            return False
        return self.manage_calculation_type(value)

    def manage_calculation_type(self, value) -> bool:
        """Return true if the dict type is a calculation
        If user set the type, it's easy, we just have to check if it's a known calculation type
        Otherwise if we have an attribute with a known calculation type, we assume that is de calculation type
        """
        if "type" in value:
            return value["type"] in CALCULATION_TYPES
        # auto set type
        typ = set(CALCULATION_TYPES) & set(value)
        # XXX variable could have identifier
        if typ == {"variable", "identifier"}:
            typ = {"variable"}
        # XXX variable is also set to information
        if typ == {"variable", "information"}:
            typ = {"information"}
        if len(typ) == 1:
            value["type"] = typ.pop()
            return True
        return False

    def set_calculation(
        self,
        attribute: str,
        value: dict,
        *,
        obj: Optional[dict] = None,
        inside_list: bool = False,
        index: int = None,
    ):
        """This variable is a calculation"""
        if obj is None:
            obj = self.parameters
        calculation_object = value.copy()
        typ = calculation_object.pop("type")
        namespace = self.objectspace.namespace

        calculation_object["attribute_name"] = attribute
        calculation_object["inside_list"] = inside_list
        calculation_object["version"] = self.version
        calculation_object["namespace"] = namespace
        calculation_object["xmlfiles"] = self.sources
        #
        self.set_identifier_calculation_in_default_attribut(
            attribute, calculation_object, inside_list, index
        )
        self.set_params_calculation(calculation_object, attribute)
        #
        return_type = calculation_object.get("return_type")
        if (
            return_type
            and return_type not in self.objectspace.variable_objects[0]["types"]
        ):
            raise Exception(
                f'unknown "return_type" in {attribute} of variable "{self.path}"'
            )
        #
        if self.raises and typ == "identifier" and not self.family_is_dynamic:
            msg = f'identifier calculation for "{attribute}" in "{self.path}" cannot be set variable is not in dynamic family'
            raise DictConsistencyError(msg, 53, self.sources)
        if attribute in PROPERTY_ATTRIBUTE:
            calc = CALCULATION_PROPERTY_TYPES[typ](**calculation_object)
        else:
            calc = CALCULATION_TYPES[typ](**calculation_object)
        if index is None:
            obj[attribute] = calc
        else:
            obj[attribute][index] = calc

    def set_identifier_calculation_in_default_attribut(
        self,
        attribute: str,
        calculation_object: dict,
        inside_list: bool,
        index: Optional[int],
    ) -> None:
        if attribute != "default" or "identifier" not in calculation_object:
            return
        identifier = calculation_object["identifier"]
        if self.is_calculation(
            "identifier",
            identifier,
            attributes_types=self.objectspace.variable_objects[0]["attributes_types"],
            inside_list=inside_list,
        ):
            self.set_calculation(
                "identifier",
                identifier,
                obj=calculation_object,
                inside_list=inside_list,
                index=index,
            )

    def set_params_calculation(self, calculation_object: dict, attribute: str) -> None:
        if "params" not in calculation_object:
            return
        params = []
        for key, value in calculation_object["params"].items():
            if isinstance(value, dict) and "type" not in value:
                self.set_param_type(value)
            if not isinstance(value, dict) or "type" not in value:
                param_typ = "any"
                value = {
                    "value": value,
                    "type": "any",
                }
            else:
                if self.version == "1.0" and value["type"] == "suffix":
                    value["type"] = "identifier"
                param_typ = value["type"]
            value["key"] = key
            value["path"] = self.path
            value["family_is_dynamic"] = self.family_is_dynamic
            value["attribute"] = attribute
            value["namespace"] = self.objectspace.namespace
            value["xmlfiles"] = self.sources
            if self.raises and param_typ not in PARAM_TYPES:
                raise DictConsistencyError(
                    f'unknown type "{param_typ}" for "{self.path}"',
                    52,
                    self.sources,
                )
            try:
                params.append(PARAM_TYPES[param_typ](**value))
            except ValidationError as err:
                if self.raises:
                    raise DictConsistencyError(
                        f'"{attribute}" has an invalid "{key}" for "{self.path}": {err}',
                        29,
                        self.sources,
                    ) from err
        calculation_object["params"] = params

    def set_param_type(self, val):
        # auto set type
        param_typ = set(CALCULATION_TYPES) & set(val)
        # XXX variable is also set to information
        if param_typ == {"variable", "information"}:
            param_typ = {"information"}
        if len(param_typ) == 1:
            val["type"] = list(param_typ)[0]

    def is_redefine(self):
        if (
            self.path in self.objectspace.paths
            and self.option_type == "family"
            and not self.parameters
        ):
            # allow loading family with no attribute loaded
            return True
        if self.types:
            return True
        return self.parameters.pop("redefine", False) or (
            self.types and list(self.parameters) in [[], ["default"]]
        )

    def is_exists(self):
        if self.version == "1.0" and self.option_type == "family":
            return None
        return self.parameters.pop("exists", None)
