"""
Cisco IOS configuration manipulation helpers

.. versionadded:: 2019.2.0

This module provides a collection of helper functions for Cisco IOS style
configuration manipulation. This module does not have external dependencies
and can be used from any Proxy or regular Minion.
"""

import difflib

# Import Salt modules
from collections import OrderedDict

import salt.utils.dictdiffer
import salt.utils.dictupdate
from salt.exceptions import SaltException

# ------------------------------------------------------------------------------
# module properties
# ------------------------------------------------------------------------------

__virtualname__ = "iosconfig"
__proxyenabled__ = ["*"]

# ------------------------------------------------------------------------------
# helper functions -- will not be exported
# ------------------------------------------------------------------------------


def _attach_data_to_path(obj, ele, data):
    if ele not in obj:
        obj[ele] = OrderedDict()
        obj[ele] = data
    else:
        obj[ele].update(data)


def _attach_data_to_path_tags(obj, path, data, list_=False):
    if "#list" not in obj:
        obj["#list"] = []
    path = [path]
    obj_tmp = obj
    first = True
    while True:
        obj_tmp["#text"] = " ".join(path)
        path_item = path.pop(0)
        if not path:
            break
        else:
            if path_item not in obj_tmp:
                obj_tmp[path_item] = OrderedDict()
            obj_tmp = obj_tmp[path_item]

            if first and list_:
                obj["#list"].append({path_item: obj_tmp})
                first = False
    if path_item in obj_tmp:
        obj_tmp[path_item].update(data)
    else:
        obj_tmp[path_item] = data
    obj_tmp[path_item]["#standalone"] = True


def _parse_text_config(config_lines, with_tags=False, current_indent=0, nested=False):
    struct_cfg = OrderedDict()
    while config_lines:
        line = config_lines.pop(0)
        if not line.strip() or line.lstrip().startswith("!"):
            # empty or comment
            continue
        current_line = line.lstrip()
        leading_spaces = len(line) - len(current_line)
        if leading_spaces > current_indent:
            current_block = _parse_text_config(
                config_lines,
                current_indent=leading_spaces,
                with_tags=with_tags,
                nested=True,
            )
            if with_tags:
                _attach_data_to_path_tags(
                    struct_cfg, current_line, current_block, nested
                )
            else:
                _attach_data_to_path(struct_cfg, current_line, current_block)
        elif leading_spaces < current_indent:
            config_lines.insert(0, line)
            break
        else:
            if not nested:
                current_block = _parse_text_config(
                    config_lines,
                    current_indent=leading_spaces,
                    with_tags=with_tags,
                    nested=True,
                )
                if with_tags:
                    _attach_data_to_path_tags(
                        struct_cfg, current_line, current_block, nested
                    )
                else:
                    _attach_data_to_path(struct_cfg, current_line, current_block)
            else:
                config_lines.insert(0, line)
                break
    return struct_cfg


def _get_diff_text(old, new):
    """
    Returns the diff of two text blobs.
    """
    diff = difflib.unified_diff(old.splitlines(1), new.splitlines(1))
    return "".join([x.replace("\r", "") for x in diff])


def _print_config_text(tree, indentation=0):
    """
    Return the config as text from a config tree.
    """
    config = ""
    for key, value in tree.items():
        config += "{indent}{line}\n".format(indent=" " * indentation, line=key)
        if value:
            config += _print_config_text(value, indentation=indentation + 1)
    return config


# ------------------------------------------------------------------------------
# callable functions
# ------------------------------------------------------------------------------


def tree(config=None, path=None, with_tags=False, saltenv="base"):
    """
    Transform Cisco IOS style configuration to structured Python dictionary.
    Depending on the value of the ``with_tags`` argument, this function may
    provide different views, valuable in different situations.

    config
        The configuration sent as text. This argument is ignored when ``path``
        is configured.

    path
        Absolute or remote path from where to load the configuration text. This
        argument allows any URI supported by
        :py:func:`cp.get_url <salt.modules.cp.get_url>`), e.g., ``salt://``,
        ``https://``, ``s3://``, ``ftp:/``, etc.

    with_tags: ``False``
        Whether this function should return a detailed view, with tags.

    saltenv: ``base``
        Salt fileserver environment from which to retrieve the file.
        Ignored if ``path`` is not a ``salt://`` URL.

    CLI Example:

    .. code-block:: bash

        salt '*' iosconfig.tree path=salt://path/to/my/config.txt
        salt '*' iosconfig.tree path=https://bit.ly/2mAdq7z
    """
    if path:
        config = __salt__["cp.get_file_str"](path, saltenv=saltenv)
        if config is False:
            raise SaltException(f"{path} is not available")
    config_lines = config.splitlines()
    return _parse_text_config(config_lines, with_tags=with_tags)


def clean(config=None, path=None, saltenv="base"):
    """
    Return a clean version of the config, without any special signs (such as
    ``!`` as an individual line) or empty lines, but just lines with significant
    value in the configuration of the network device.

    config
        The configuration sent as text. This argument is ignored when ``path``
        is configured.

    path
        Absolute or remote path from where to load the configuration text. This
        argument allows any URI supported by
        :py:func:`cp.get_url <salt.modules.cp.get_url>`), e.g., ``salt://``,
        ``https://``, ``s3://``, ``ftp:/``, etc.

    saltenv: ``base``
        Salt fileserver environment from which to retrieve the file.
        Ignored if ``path`` is not a ``salt://`` URL.

    CLI Example:

    .. code-block:: bash

        salt '*' iosconfig.clean path=salt://path/to/my/config.txt
        salt '*' iosconfig.clean path=https://bit.ly/2mAdq7z
    """
    config_tree = tree(config=config, path=path, saltenv=saltenv)
    return _print_config_text(config_tree)


def merge_tree(
    initial_config=None,
    initial_path=None,
    merge_config=None,
    merge_path=None,
    saltenv="base",
):
    """
    Return the merge tree of the ``initial_config`` with the ``merge_config``,
    as a Python dictionary.

    initial_config
        The initial configuration sent as text. This argument is ignored when
        ``initial_path`` is set.

    initial_path
        Absolute or remote path from where to load the initial configuration
        text. This argument allows any URI supported by
        :py:func:`cp.get_url <salt.modules.cp.get_url>`), e.g., ``salt://``,
        ``https://``, ``s3://``, ``ftp:/``, etc.

    merge_config
        The config to be merged into the initial config, sent as text. This
        argument is ignored when ``merge_path`` is set.

    merge_path
        Absolute or remote path from where to load the merge configuration
        text. This argument allows any URI supported by
        :py:func:`cp.get_url <salt.modules.cp.get_url>`), e.g., ``salt://``,
        ``https://``, ``s3://``, ``ftp:/``, etc.

    saltenv: ``base``
        Salt fileserver environment from which to retrieve the file.
        Ignored if ``initial_path`` or ``merge_path`` is not a ``salt://`` URL.

    CLI Example:

    .. code-block:: bash

        salt '*' iosconfig.merge_tree initial_path=salt://path/to/running.cfg merge_path=salt://path/to/merge.cfg
    """
    merge_tree = tree(config=merge_config, path=merge_path, saltenv=saltenv)
    initial_tree = tree(config=initial_config, path=initial_path, saltenv=saltenv)
    return salt.utils.dictupdate.merge(initial_tree, merge_tree)


def merge_text(
    initial_config=None,
    initial_path=None,
    merge_config=None,
    merge_path=None,
    saltenv="base",
):
    """
    Return the merge result of the ``initial_config`` with the ``merge_config``,
    as plain text.

    initial_config
        The initial configuration sent as text. This argument is ignored when
        ``initial_path`` is set.

    initial_path
        Absolute or remote path from where to load the initial configuration
        text. This argument allows any URI supported by
        :py:func:`cp.get_url <salt.modules.cp.get_url>`), e.g., ``salt://``,
        ``https://``, ``s3://``, ``ftp:/``, etc.

    merge_config
        The config to be merged into the initial config, sent as text. This
        argument is ignored when ``merge_path`` is set.

    merge_path
        Absolute or remote path from where to load the merge configuration
        text. This argument allows any URI supported by
        :py:func:`cp.get_url <salt.modules.cp.get_url>`), e.g., ``salt://``,
        ``https://``, ``s3://``, ``ftp:/``, etc.

    saltenv: ``base``
        Salt fileserver environment from which to retrieve the file.
        Ignored if ``initial_path`` or ``merge_path`` is not a ``salt://`` URL.

    CLI Example:

    .. code-block:: bash

        salt '*' iosconfig.merge_text initial_path=salt://path/to/running.cfg merge_path=salt://path/to/merge.cfg
    """
    candidate_tree = merge_tree(
        initial_config=initial_config,
        initial_path=initial_path,
        merge_config=merge_config,
        merge_path=merge_path,
        saltenv=saltenv,
    )
    return _print_config_text(candidate_tree)


def merge_diff(
    initial_config=None,
    initial_path=None,
    merge_config=None,
    merge_path=None,
    saltenv="base",
):
    """
    Return the merge diff, as text, after merging the merge config into the
    initial config.

    initial_config
        The initial configuration sent as text. This argument is ignored when
        ``initial_path`` is set.

    initial_path
        Absolute or remote path from where to load the initial configuration
        text. This argument allows any URI supported by
        :py:func:`cp.get_url <salt.modules.cp.get_url>`), e.g., ``salt://``,
        ``https://``, ``s3://``, ``ftp:/``, etc.

    merge_config
        The config to be merged into the initial config, sent as text. This
        argument is ignored when ``merge_path`` is set.

    merge_path
        Absolute or remote path from where to load the merge configuration
        text. This argument allows any URI supported by
        :py:func:`cp.get_url <salt.modules.cp.get_url>`), e.g., ``salt://``,
        ``https://``, ``s3://``, ``ftp:/``, etc.

    saltenv: ``base``
        Salt fileserver environment from which to retrieve the file.
        Ignored if ``initial_path`` or ``merge_path`` is not a ``salt://`` URL.

    CLI Example:

    .. code-block:: bash

        salt '*' iosconfig.merge_diff initial_path=salt://path/to/running.cfg merge_path=salt://path/to/merge.cfg
    """
    if initial_path:
        initial_config = __salt__["cp.get_file_str"](initial_path, saltenv=saltenv)
    candidate_config = merge_text(
        initial_config=initial_config,
        merge_config=merge_config,
        merge_path=merge_path,
        saltenv=saltenv,
    )
    clean_running_dict = tree(config=initial_config)
    clean_running = _print_config_text(clean_running_dict)
    return _get_diff_text(clean_running, candidate_config)


def diff_tree(
    candidate_config=None,
    candidate_path=None,
    running_config=None,
    running_path=None,
    saltenv="base",
):
    """
    Return the diff, as Python dictionary, between the candidate and the running
    configuration.

    candidate_config
        The candidate configuration sent as text. This argument is ignored when
        ``candidate_path`` is set.

    candidate_path
        Absolute or remote path from where to load the candidate configuration
        text. This argument allows any URI supported by
        :py:func:`cp.get_url <salt.modules.cp.get_url>`), e.g., ``salt://``,
        ``https://``, ``s3://``, ``ftp:/``, etc.

    running_config
        The running configuration sent as text. This argument is ignored when
        ``running_path`` is set.

    running_path
        Absolute or remote path from where to load the running configuration
        text. This argument allows any URI supported by
        :py:func:`cp.get_url <salt.modules.cp.get_url>`), e.g., ``salt://``,
        ``https://``, ``s3://``, ``ftp:/``, etc.

    saltenv: ``base``
        Salt fileserver environment from which to retrieve the file.
        Ignored if ``candidate_path`` or ``running_path`` is not a
        ``salt://`` URL.

    CLI Example:

    .. code-block:: bash

        salt '*' iosconfig.diff_tree candidate_path=salt://path/to/candidate.cfg running_path=salt://path/to/running.cfg
    """
    candidate_tree = tree(config=candidate_config, path=candidate_path, saltenv=saltenv)
    running_tree = tree(config=running_config, path=running_path, saltenv=saltenv)
    return salt.utils.dictdiffer.deep_diff(running_tree, candidate_tree)


def diff_text(
    candidate_config=None,
    candidate_path=None,
    running_config=None,
    running_path=None,
    saltenv="base",
):
    """
    Return the diff, as text, between the candidate and the running config.

    candidate_config
        The candidate configuration sent as text. This argument is ignored when
        ``candidate_path`` is set.

    candidate_path
        Absolute or remote path from where to load the candidate configuration
        text. This argument allows any URI supported by
        :py:func:`cp.get_url <salt.modules.cp.get_url>`), e.g., ``salt://``,
        ``https://``, ``s3://``, ``ftp:/``, etc.

    running_config
        The running configuration sent as text. This argument is ignored when
        ``running_path`` is set.

    running_path
        Absolute or remote path from where to load the running configuration
        text. This argument allows any URI supported by
        :py:func:`cp.get_url <salt.modules.cp.get_url>`), e.g., ``salt://``,
        ``https://``, ``s3://``, ``ftp:/``, etc.

    saltenv: ``base``
        Salt fileserver environment from which to retrieve the file.
        Ignored if ``candidate_path`` or ``running_path`` is not a
        ``salt://`` URL.

    CLI Example:

    .. code-block:: bash

        salt '*' iosconfig.diff_text candidate_path=salt://path/to/candidate.cfg running_path=salt://path/to/running.cfg
    """
    candidate_text = clean(
        config=candidate_config, path=candidate_path, saltenv=saltenv
    )
    running_text = clean(config=running_config, path=running_path, saltenv=saltenv)
    return _get_diff_text(running_text, candidate_text)
