"""
Silique (https://www.silique.fr)
Copyright (C) 2024-2025

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
from itertools import chain
from re import compile

from tiramisu import Calculation, undefined, groups
from tiramisu.error import ConfigError, display_list
from rougail.error import display_xmlfiles
from rougail.object_model import PROPERTY_ATTRIBUTE

from .config import OutPuts
from .i18n import _
from .utils import DocTypes, get_display_path, dump
from .example import Examples


class RougailOutputDoc(Examples):
    """Rougail Output Doc:
    Generate documentation from rougail description files
    """

    def __init__(
        self,
        config: "Config",
        *,
        rougailconfig: "RougailConfig" = None,
        **kwarg,
    ):
        # Import here to avoid circular import
        from rougail import CONVERT_OPTION

        self.convert_option = CONVERT_OPTION
        if rougailconfig is None:
            from rougail import RougailConfig

            rougailconfig = RougailConfig
            if rougailconfig["step.output"] != "doc":
                rougailconfig["step.output"] = "doc"
        if rougailconfig["step.output"] != "doc":
            raise Exception("doc is not set as step.output")
        outputs = OutPuts().get()
        output = rougailconfig["doc.output_format"]
        if output not in outputs:
            raise Exception(
                f'cannot find output "{output}", available outputs: {list(outputs)}'
            )
        self.conf = config
        self.modes_level = rougailconfig["modes_level"]
        if self.modes_level:
            self.disabled_modes = rougailconfig["doc.disabled_modes"]
            if self.disabled_modes:
                self.conf.property.setdefault(
                    frozenset(self.disabled_modes), "read_write", "append"
                )
        else:
            self.disabled_modes = []
        self.conf.property.read_write()
        # self.conf.property.remove("cache")
        self.formater = outputs[output]()
        self.level = rougailconfig["doc.title_level"]
        self.dynamic_paths = {}
        self.example = rougailconfig["doc.example"]
        self.with_family = not rougailconfig["doc.without_family"]
        self.informations = None
        try:
            groups.namespace
            self.support_namespace = True
        except AttributeError:
            self.support_namespace = False
        self.property_to_string = [
            ("mandatory", _("mandatory")),
            ("hidden", _("hidden")),
            ("disabled", _("disabled")),
            ("unique", _("unique")),
            ("force_store_value", _("auto modified")),
        ]
        super().__init__()

    def run(self) -> str:
        """Print documentation in stdout"""
        self._tiramisu_to_internal_object()
        if not self.example:
            return_string = self.formater.run(self.informations, self.level)
        else:
            return_string = self.gen_doc_examples()
        return True, return_string

    def print(self) -> None:
        ret, data = self.run()
        print(data)
        return ret

    def _tiramisu_to_internal_object(self):
        config = self.conf.unrestraint
        self._populate_dynamics(config)
        informations = self._parse_families(config)
        if informations is None:
            informations = {}
        self.informations = informations

    def _populate_dynamics(self, family) -> None:
        for child in family.list():
            path = child.path(uncalculated=True)
            if not child.isoptiondescription():
                func = self._populate_dynamic_variable
            else:
                func = self._populate_dynamic_family
            func(child, path)

    def _populate_dynamic_variable(self, variable, path) -> None:
        if not variable.isdynamic():
            return
        if path not in self.dynamic_paths:
            self.dynamic_paths[path] = {"paths": [], "names": []}
        self._dyn_path_to_italic(self.dynamic_paths[path], variable, path)
        self.dynamic_paths[path]["names"].append(variable.name())

    def _populate_dynamic_family(self, family, path) -> None:
        if family.isdynamic():
            if path not in self.dynamic_paths:
                self.dynamic_paths[path] = {"paths": [], "names": []}
            self._dyn_path_to_italic(self.dynamic_paths[path], family, path)
            self.dynamic_paths[path]["names"].append(family.name())
        self._populate_dynamics(family)

    def _parse_families(self, family) -> dict:
        informations = {}
        leader = None
        for child in family.list():
            if self._is_inaccessible_user_data(child):
                continue
            if child.type(only_self=True) == "symlink":
                continue
            name = child.name(uncalculated=True)
            path = child.path(uncalculated=True)
            if not child.isoptiondescription():
                leader = self._parse_variable(child, leader, name, path, informations)
            else:
                self._parse_family(child, informations, name, path)
        return informations

    def _is_inaccessible_user_data(self, child):
        """If family is not accessible in read_write mode (to load user_data),
        do not comment this family
        """
        properties = child.property.get(uncalculated=True)
        for hidden_property in [
            "hidden",
            "disabled",
        ]:  # chain(["hidden", "disabled"], self.disabled_modes):
            if hidden_property in properties:
                return True

            calculation = child.information.get(f"{hidden_property}_calculation", None)
            if calculation and calculation['type'] == 'variable':
                variable_path, value, condition = calculation["value"]
                variable = self.conf.forcepermissive.option(variable_path)
                try:
                    variable_value = variable.value.get()
                except AttributeError as err:
                    pass
                else:
                    uncalculated = variable.value.get(uncalculated=True)
                    if not isinstance(uncalculated, Calculation) and self._is_inaccessible_user_data(variable) and (condition == 'when' and value == variable_value or
                                condition == 'when_not' and value != variable_value):
                        return True
        if not child.isoptiondescription():
            for hidden_property in self.disabled_modes:
                if hidden_property in properties:
                    return True
        return False

    def _parse_family(self, family, informations: dict, name: str, path: str) -> None:
        sub_informations = self._parse_families(family)
        if not sub_informations:
            return
        if self.with_family:
            informations[name] = {
                "type": self._get_family_type(family),
                "informations": self._populate_family(
                    family,
                    path,
                ),
                "children": sub_informations,
            }
        else:
            informations.update(sub_informations)

    def _parse_variable(
        self, variable, leader: dict, name: str, path: str, informations: dict
    ) -> Optional[dict]:
        if variable.isdynamic():
            # information is already set
            potential_leader = self._parse_variable_dynamic(
                variable, leader, name, path, informations
            )
        elif variable.isfollower() and variable.index():
            potential_leader = self._parse_variable_follower_with_index(
                variable, path, informations
            )
        else:
            potential_leader = self._parse_variable_normal(
                variable, leader, name, path, informations
            )
        if potential_leader:
            leader = potential_leader
        return leader

    def _parse_variable_normal(
        self, variable, leader, name: str, path: str, informations: dict
    ) -> Optional[dict]:
        if variable.isdynamic():
            sub_informations = self.dynamic_paths[path]
        else:
            sub_informations = {}
        self._populate_variable(
            variable,
            sub_informations,
        )
        if self.example:
            self._add_examples(variable, sub_informations, leader)
        informations[path] = sub_informations
        if variable.isleader():
            return sub_informations
        return None

    def _parse_variable_follower_with_index(
        self, variable, path: str, informations: dict
    ) -> None:
        if not self.example:
            return None
        informations[path]["example"][-1][variable.index()] = self._get_example(
            variable, informations[path], None
        )
        return None

    def _parse_variable_dynamic(
        self, variable, leader, name, path, informations
    ) -> None:
        dynamic_variable = self.dynamic_paths[path]
        if "type" in dynamic_variable:
            if self.example:
                dynamic_variable["example"].append(
                    self._get_example(variable, dynamic_variable, leader)
                )
            description = self.formater.to_phrase(variable.description(uncalculated=True))
            if "{{ identifier }}" in description:
                description = self._convert_description(description, variable)
                dynamic_variable["descriptions"].append(self.formater.to_phrase(description))
            if variable.isleader():
                return dynamic_variable
            return None
        return self._parse_variable_normal(variable, leader, name, path, informations)

    def _get_family_type(self, family) -> str:
        if self.support_namespace and family.group_type() is groups.namespace:
            return "namespace"
        if family.isleadership():
            return "leadership"
        if family.isdynamic(only_self=True):
            return "dynamic"
        return "family"

    def _dyn_path_to_italic(self, dico, child, path: str) -> str:
        display_path = path
        for identifier in child.identifiers():
            display_path = display_path.replace(
                "{{ identifier }}", self.formater.italic(identifier), 1
            )
            path = path.replace("{{ identifier }}", str(identifier), 1)
        if display_path != path:
            if "display_paths" not in dico:
                dico["display_paths"] = {}
            dico["display_paths"][len(dico["paths"])] = display_path
        dico["paths"].append(path)

    def _populate_family(
        self,
        family,
        path: str,
    ) -> dict:
        if family.isdynamic():
            informations = self.dynamic_paths[path]
        else:
            informations = {}
        self._populate(family, informations)
        if family.isleadership():
            informations.setdefault("help", []).append(
                _("This family contains lists of variable blocks.")
            )
        if family.isdynamic(only_self=True):
            identifiers = self._to_string(family, "dynamic", do_not_raise=True)
            if identifiers is None:
                identifiers = family.identifiers(only_self=True)
            informations["identifiers"] = identifiers
            informations.setdefault("help", []).append(
                _("This family builds families dynamically.")
            )
        return informations

    def _populate_variable(
        self,
        variable,
        informations: dict,
    ):
        informations["type"] = "variable"
        default = self._get_default(
            variable,
        )
        if default is not None:
            informations["default"] = default
        self._parse_type(
            variable,
            informations,
        )
        self._populate(variable, informations)
        if "description" in informations:
            informations["descriptions"] = [self.formater.to_phrase(informations.pop("description"))]
        if variable.ismulti():
            multi = not variable.isfollower() or variable.issubmulti()
        else:
            multi = False
        if multi:
            informations["properties"].append(
                {
                    "type": "multiple",
                    "name": _("multiple"),
                }
            )
        examples = variable.information.get("examples", None)
        if examples is None:
            examples = variable.information.get("test", None)
        if examples is not None:
            informations["examples"] = list(examples)

    def _populate(
        self,
        obj,
        informations: dict,
    ):
        if not obj.isdynamic():
            informations["paths"] = [obj.path(uncalculated=True)]
            informations["names"] = [obj.name()]
        description = obj.description(uncalculated=True)
        if obj.name(uncalculated=True) == description and (
            not obj.isoptiondescription() or (self.support_namespace and obj.group_type() is not groups.namespace)
        ):
            warning = _('No attribute "description" for "{0}" in {1}').format(
                obj.path(uncalculated=True),
                display_xmlfiles(obj.information.get("ymlfiles")),
            )
            warn(warning)
        else:
            informations["description"] = self._convert_description(description, obj)
        help_ = obj.information.get("help", None)
        if help_:
            informations["help"] = [self.formater.to_phrase(help_)]
        self._parse_properties(
            obj,
            informations,
        )

    def _convert_description(self, description, obj):
        if "{{ identifier }}" in description:
            return description.replace(
                "{{ identifier }}", self.formater.italic(obj.identifiers()[-1])
            )
        return description

    def _add_examples(self, variable, informations: dict, leader) -> None:
        example = self._get_example(variable, informations, leader)
        informations["example"] = [example]
        informations["mandatory_without_value"] = "mandatory" in variable.property.get(
            uncalculated=True
        ) and (not variable.information.get("default_value_makes_sense", True) or variable.value.get(uncalculated=True) in [None, []])

    def _get_example(self, variable, informations: dict, leader):
        example = informations.get("examples")
        if example is not None:
            if isinstance(example, tuple):
                example = list(example)
            for prop in informations["properties"]:
                if prop["type"] == "multiple":
                    multi = True
                    break
            else:
                multi = False
            if not multi:
                example = example[0]
        else:
            if variable.information.get("fake_default", False):
                default = None
            else:
                try:
                    default = variable.value.get()
                except ConfigError:
                    default = None
            if default not in [None, []]:
                example = default
            else:
                example = self.convert_option.get(
                    variable.information.get("type"), {}
                ).get("example", None)
                if example is None:
                    example = "xxx"
                for prop in informations["properties"]:
                    if prop["type"] == "multiple":
                        multi = True
                        break
                else:
                    multi = False
                if multi:
                    example = [example]
        if leader is not None and variable.isfollower():
            example = [example] + [undefined] * (len(leader["example"][-1]) - 1)
        return example

    def _parse_type(
        self,
        variable,
        informations,
    ):
        variable_type = variable.information.get("type")
        doc_type = DocTypes.get(variable_type, {"params": {}})
        informations["properties"] = [
            {
                "type": "type",
                "name": doc_type.get("msg", variable_type),
            }
        ]
        # extra parameters for types
        option = variable.get()
        for param, msg in doc_type["params"].items():
            value = option.impl_get_extra(f"_{param}")
            if value is None:
                value = option.impl_get_extra(param)
            if value is not None and value is not False:
                if isinstance(value, set):
                    value = list(value)
                if isinstance(value, list):
                    value = display_list(value, add_quote=True)
                informations.setdefault("validators", []).append(msg.format(value))
        # get validation information from annotator
        for name in variable.information.list():
            if not name.startswith("validators_calculation"):
                continue
            informations.setdefault("validators", []).extend(
                self._to_string(
                    variable,
                    "validators",
                )
            )
            break
        if variable.information.get("type") == "choice":
            choices = self._to_string(variable, "choice", do_not_raise=True)
            if choices is None:
                choices = variable.value.list()
                for idx, val in enumerate(choices):
                    if not isinstance(val, Calculation):
                        default = informations.get("default")
                        if default is not None and val == default:
                            choices[idx] = str(val) + " " + self.formater.bold("← " + _("(default)"))
                            informations["display_default"] = False
                        continue
                    choices[idx] = self._to_string(variable, "choice", f"_{idx}")
            informations["choices"] = choices
        if variable.information.get("type") == "regexp":
            informations.setdefault("validators", []).append(
                _('text based with regular expressions "{0}"').format(
                    variable.pattern()
                )
            )

    def _parse_properties(
        self,
        variable,
        informations,
    ):
        properties = variable.property.get(uncalculated=True)
        for mode in self.modes_level:
            if mode not in properties:
                continue
            informations.setdefault("properties", []).append(
                {
                    "type": "mode",
                    "name": mode,
                }
            )
            break
        for prop, msg in self.property_to_string:
            if prop in properties:
                prop_obj = {
                    "type": "property",
                    "name": msg,
                }
            elif variable.information.get(f"{prop}_calculation", False):
                annotation = self._to_string(variable, prop)
                if not annotation:
                    continue
                prop_obj = {
                    "type": "property",
                    "name": msg,
                    "annotation": annotation,
                }
            else:
                continue
            informations.setdefault("properties", []).append(prop_obj)

    def _get_default(
        self,
        variable,
    ):
        default = self._to_string(variable, "default", do_not_raise=True)
        if default is not None:
            if default == []:
                default = None
            return default
        if variable.information.get("default_value_makes_sense", True):
            default = variable.value.get(uncalculated=True)
        if default == []:
            default = None
        return default

    def _to_string(
        self,
        variable,
        prop,
        do_not_raise=False,
    ):
        calculation = variable.information.get(f"{prop}_calculation", None)
        if not calculation:
            if do_not_raise:
                return None
            raise Exception(
                f'cannot find "{prop}_calculation" information, '
                "do you have declare doc has a plugins?"
            )
        #        if do_not_raise and calculation.get('optional', False):
        #            return None
        if isinstance(calculation, list):
            values = []
            for cal in calculation:
                value = self._calculation_to_string(variable, cal, prop)
                if value is not None:
                    values.append(value)
            return values
        return self._calculation_to_string(variable, calculation, prop)

    def _calculation_to_string(self, child, calculation, prop):
        if "type" not in calculation:
            return calculation["value"]
        if calculation["type"] == "jinja":
            if calculation["value"] is not True:
                values = calculation["value"]
            else:
                values = _("depends on a calculation")
                warning = _(
                    '"{0}" is a calculation for {1} but has no description in {2}'
                ).format(
                    prop,
                    child.path(),
                    display_xmlfiles(child.information.get("ymlfiles")),
                )
                warn(warning)
        elif calculation["type"] == "variable":
            if prop in PROPERTY_ATTRIBUTE:
                variable_path, value, condition = calculation["value"]
                variable = self.conf.forcepermissive.option(variable_path)
                try:
                    variable.value.get()
                except AttributeError as err:
                    pass
                    variable = None
                else:
                    uncalculated = variable.value.get(uncalculated=True)
                    if not isinstance(uncalculated, Calculation) and self._is_inaccessible_user_data(variable):
                        return None
                if variable and self._is_inaccessible_user_data(variable):
                    msg = _("depends on an undocumented variable")
                elif condition == "when_not":
                    msg = _('when the variable "{0}" hasn\'t the value "{1}"')
                else:
                    msg = _('when the variable "{0}" has the value "{1}"')
                if not isinstance(value, str):
                    value = dump(value)
                values = msg.format(variable_path, value)
            else:
                if calculation.get("optional", False):
                    path = calculation["value"]
                    if "{{ identifier }}" in path:
                        if path not in self.dynamic_paths:
                            return None
                    else:
                        try:
                            self.conf.option(path).get()
                        except AttributeError:
                            return None
                true_msg = _('the value of the variable "{0}"')
                hidden_msg = _("the value of an undocumented variable")
                # if "{{ identifier }}" in calculation["value"] and calculation["value"] in self.dynamic_paths:
                if "{{ identifier }}" in calculation["ori_path"]:
                    if calculation["value"] == calculation['ori_path']:
                        regexp = None
                    else:
                        regexp = compile("^" + calculation["ori_path"].replace("{{ identifier }}", "(.*)") + "$")
                    informations = [self.dynamic_paths[calculation["value"]]]
                    values = []
                    all_is_undocumented = None
                    for information in informations:
                        for idx, path in enumerate(information["paths"]):
                            if regexp and not regexp.search(path):
                                continue
                            if self._is_inaccessible_user_data(self.conf.option(path)):
                                if all_is_undocumented is None:
                                    all_is_undocumented = True
                                msg = hidden_msg
                            else:
                                if regexp:
                                    display_path = calculation['ori_path']
                                    for identifier in regexp.findall(path):
                                        display_path = display_path.replace(
                                            "{{ identifier }}", self.formater.italic(identifier), 1
                                        )
                                else:
                                    display_path = get_display_path(information, idx)
                                msg = true_msg.format(display_path)
                                all_is_undocumented = False
                            values.append(msg)
                    if all_is_undocumented and len(values) > 1:
                        values = _("the values of undocumented variables")
                else:
                    values = true_msg.format(calculation["ori_path"])
        elif calculation["type"] == "identifier":
            if prop in PROPERTY_ATTRIBUTE:
                values = calculation["value"]
            else:
                values = _("the value of the identifier")
        elif calculation["type"] == "information":
            values = calculation["value"]
        else:
            values = _("the value of the {0}").format(calculation["type"])
        if isinstance(values, str) and not values.endswith("."):
            values += "."
        return values
