"""
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 typing import List

from io import BytesIO
from ruamel.yaml import YAML
import tabulate as tabulate_module
from tiramisu.error import display_list
from tabulate import tabulate

from .i18n import _


ROUGAIL_VARIABLE_TYPE = (
    "https://rougail.readthedocs.io/en/latest/variable.html#variables-types"
)


ENTER = "\n\n"


DocTypes = {
    "domainname": {
        "params": {
            "allow_startswith_dot": _("the domain name can starts by a dot"),
            "allow_without_dot": _("the domain name can be a hostname"),
            "allow_ip": _("the domain name can be an IP"),
            "allow_cidr_network": _("the domain name can be network in CIDR format"),
        },
    },
    "number": {
        "params": {
            "min_number": _("the minimum value is {0}"),
            "max_number": _("the maximum value is {0}"),
        },
    },
    "integer": {
        "params": {
            "min_integer": _("the minimum value is {0}"),
            "max_integer": _("the maximum value is {0}"),
        },
    },
    "ip": {
        "msg": "IP",
        "params": {
            "cidr": _("IP must be in CIDR format"),
            "private_only": _("private IP are allowed"),
            "allow_reserved": _("reserved IP are allowed"),
        },
    },
    "network": {
        "params": {
            "cidr": _("network must be in CIDR format"),
        },
    },
    "hostname": {
        "params": {
            "allow_ip": _("the host name can be an IP"),
        },
    },
    "web_address": {
        "params": {
            "allow_ip": _("the domain name in web address can be an IP"),
            "allow_without_dot": _(
                "the domain name in web address can be only a hostname"
            ),
        },
    },
    "port": {
        "params": {
            "allow_range": _("can be range of port"),
            "allow_protocol": _("can have the protocol"),
            "allow_zero": _("port 0 is allowed"),
            "allow_wellknown": _("well-known ports (1 to 1023) are allowed"),
            "allow_registred": _("registred ports (1024 to 49151) are allowed"),
            "allow_private": _("private ports (greater than 49152) are allowed"),
        },
    },
    "secret": {
        "params": {
            "min_len": _("minimum length for the secret is {0} characters"),
            "max_len": _("maximum length for the secret is {0} characters"),
            "forbidden_char": _("forbidden characters: {0}"),
        },
    },
    "unix_filename": {
        "params": {
            "allow_relative": _("this filename could be a relative path"),
            "test_existence": _("this file must exists"),
            "types": _("file type allowed: {0}"),
        },
    },
}


_yaml = YAML()
_yaml.indent(mapping=2, sequence=4, offset=2)


def dump(informations):
    """Dump variable, means transform bool, ... to yaml string"""
    with BytesIO() as ymlfh:
        _yaml.dump(informations, ymlfh)
        ret = ymlfh.getvalue().decode("utf-8").strip()
    if ret.endswith("..."):
        ret = ret[:-3].strip()
    return ret


def to_phrase(msg):
    """Add maj for the first character and ends with dot"""
    if not msg:
        # replace None to empty string
        return ""
    msg = str(msg).strip()
    # a phrase must ends with a dot
    if not msg.endswith("."):
        msg += "."
    # and start with a maj
    return msg[0].upper() + msg[1:]


class CommonFormater:
    """Class with common function for formater"""

    enter_table = "\n"
    # tabulate module name
    name = None

    def __init__(self):
        tabulate_module.PRESERVE_WHITESPACE = True
        self.header_setted = False

    # Class you needs implement to your Formater
    def title(
        self,
        title: str,
        level: int,
    ) -> str:
        """Display family name as a title"""
        raise NotImplementedError()

    def join(
        self,
        lst: List[str],
    ) -> str:
        """Display line in table from a list"""
        raise NotImplementedError()

    def bold(
        self,
        msg: str,
    ) -> str:
        """Set a text to bold"""
        raise NotImplementedError()

    def stripped(
        self,
        text: str,
    ) -> str:
        """Return stripped text (as help)"""
        raise NotImplementedError()

    def list(
        self,
        choices: list,
    ) -> str:
        """Display a liste of element"""
        raise NotImplementedError()

    def prop(
        self,
        prop: str,
        italic: bool,
    ) -> str:
        """Display property"""
        raise NotImplementedError()

    def link(
        self,
        comment: str,
        link: str,
    ) -> str:
        """Add a link"""
        raise NotImplementedError()

    ##################

    def family_informations(self) -> str:
        return ''

    def end_family_informations(self) -> str:
        return ''

    def display_path(
        self,
        path: str,
    ) -> str:
        return self.bold(path)

    def after_family_paths(self) -> str:
        return ENTER

    def after_family_properties(self) -> str:
        return ENTER

    def table_header(
        self,
        lst: list,
    ) -> tuple:
        """Manage the header of a table"""
        return lst

    def run(self, informations: dict, level: int) -> str:
        """Transform to string"""
        if informations:
            return self.dict_to_string(informations, level)
        return ""

    def dict_to_dict(self, dico: dict, level: int) -> str:
        """Parse the dict to transform to dict"""
        msg = []
        table_datas = []
        ori_level = None
        for value in dico.values():
            if value["type"] == "namespace":
                if ori_level is None:
                    ori_level = level
                    level += 1
                informations = value["informations"]
                msg.append(self.namespace_to_title(informations, ori_level))
                msg.append(self.family_informations())
                msg.append(self.display_path(get_display_path(informations, 0)))
                msg.append(self.after_family_paths())
                msg.append(self.property_to_string(informations, {}) + ENTER)
                msg.append(self.end_family_informations())
                msg.extend(self.dict_to_dict(value["children"], level))
                msg.append(self.end_namespace())
            else:
                if value["type"] == "variable":
                    self.variable_to_string(value, table_datas)
                else:
                    if table_datas:
                        msg.append(self.table(table_datas))
                        table_datas = []
                    msg.extend(self.family_to_string(value["informations"], level))
                    msg.extend(self.dict_to_dict(value["children"], level + 1))
                    msg.append(self.end_family())
        if table_datas:
            msg.append(self.table(table_datas))
        return msg

    def dict_to_string(self, dico: dict, level: int) -> str:
        """Parse the dict to transform to dict"""
        return "".join([msg for msg in self.dict_to_dict(dico, level)])

    # FAMILY
    def namespace_to_title(self, informations: dict, level: int) -> str:
        """manage namespace family"""
        return self.title(
            _('Variables for "{0}"').format(self.family_description(informations)),
            level,
        )

    def end_namespace(self) -> str:
        return self.end_family()

    def family_to_string(self, informations: dict, level: int) -> str:
        """manage other family type"""
        msg = [self.title(self.family_description(informations), level)]
        helps = informations.get("help")
        if helps:
            for help_ in helps:
                msg.append(self.display_family_help(help_.strip()))
        msg.append(self.family_informations())
        msg.append(self.join(
                [
                    self.display_path(get_display_path(informations, index))
                    for index in range(len(informations["paths"]))
                ]
            ) + self.after_family_paths()
        )
        calculated_properties = []
        msg.append(self.property_to_string(informations, calculated_properties) + ENTER)
        if calculated_properties:
            msg.append(self.join(calculated_properties) + self.after_family_properties())
        if "identifiers" in informations:
            msg.append(
                self.section(_("Identifiers"), informations["identifiers"]) + self.after_family_properties()
            )
        msg.append(self.end_family_informations())
        return msg

    def end_family(self):
        return ''

    def family_description(self, informations: dict) -> str():
        """Get family name"""
        if "description" in informations:
            return informations["description"]
        return display_list(
            [
                get_display_path(informations, index)
                for index in range(len(informations["paths"]))
            ],
            separator="or",
        )

    # VARIABLE
    def variable_to_string(self, informations: dict, table_datas: dict) -> None:
        """Manage variable"""
        calculated_properties = []
        table_datas.append(
            [
                self.join(
                    self.variable_first_column(informations, calculated_properties)
                ),
                self.join(
                    self.variable_second_column(informations, calculated_properties)
                ),
            ]
        )

    def variable_first_column(
        self, informations: dict, calculated_properties: list
    ) -> list:
        """Collect string for the first column"""
        first_col = [
            self.join(
                [
                    self.display_path(get_display_path(informations, index))
                    for index in range(len(informations["paths"]))
                ]
            ),
            self.property_to_string(informations, calculated_properties),
        ]
        self.columns(first_col)
        return first_col

    def variable_second_column(
        self, informations: dict, calculated_properties: list
    ) -> list:
        """Collect string for the second column"""
        if "descriptions" in informations:
            description = self.join(list(dict.fromkeys(informations["descriptions"])))
        else:
            description = to_phrase(
                display_list(
                    list(dict.fromkeys(informations["names"])),
                    separator="or",
                )
            )
        second_col = [self.stripped(description)]
        for help_ in informations.get("help", []):
            second_col.append(self.stripped(help_))
        if "validators" in informations:
            validators = informations["validators"]
            if isinstance(validators["values"], list):
                values = self.list(validators["values"])
            else:
                values = validators["values"]
            second_col.append(self.section(validators["name"], values))
        if "choices" in informations:
            choices = informations["choices"]
            second_col.append(self.section(choices["name"], choices["values"]))
        if (
            "default" in informations
            and informations.get("default_is_already_set", False) is False
        ):
            default = informations["default"]
            second_col.append(self.section(default["name"], default["values"]))
        if "examples" in informations:
            examples = informations["examples"]
            if isinstance(examples["values"], list):
                values = self.list(examples["values"])
            else:
                values = examples["values"]
            second_col.append(self.section(examples["name"], values))
        second_col.extend(calculated_properties)
        self.columns(second_col)
        return second_col

    # OTHERs
    def display_family_help(self, help_):
        return help_ + ENTER

    def property_to_string(
        self, informations: dict, calculated_properties: list
    ) -> str:
        """Transform properties to string"""
        properties = []
        for prop in informations.get("properties", []):
            if prop["type"] == "type":
                properties.append(self.link(prop["name"], ROUGAIL_VARIABLE_TYPE))
            else:
                if "annotation" in prop:
                    italic = True
                    calculated_properties.append(
                        self.section(prop["name"].capitalize(), prop["annotation"])
                    )
                else:
                    italic = False
                prop_str = self.prop(prop["name"], italic=italic)
                properties.append(prop_str)
        if not properties:
            return ""
        return " ".join(properties)

    def columns(
        self,
        col: List[str],  # pylint: disable=unused-argument
    ) -> None:
        """Manage column"""
        return

    def table(self, datas: list) -> str:
        """Transform list to a table in string format"""
        msg = (
            tabulate(
                datas,
                headers=self.table_header([_("Variable"), _("Description")]),
                tablefmt=self._table_name,
            )
            + "\n\n"
        )
        datas.clear()
        return msg

    def section(
        self,
        name: str,
        msg: str,
    ) -> str:
        """Return something like Name: msg"""
        if isinstance(msg, list):
            if len(msg) == 1:
                msg = msg[0]
            else:
                msg = self.list(msg)
        if not isinstance(msg, str):
            msg = dump(msg)
        return _("{0}: {1}").format(self.bold(name), msg)


def get_display_path(informations: dict, index: int) -> str:
    if "display_paths" in informations and index in informations["display_paths"]:
        return informations["display_paths"][index]
    return informations["paths"][index]
