"""Helper functions for the NAPALM base."""
import itertools
import logging

# std libs
import os
import re
import sys
from typing import Optional, Dict, Any, List, Union, Tuple, TypeVar, Callable
from collections.abc import Iterable

# third party libs
import jinja2
import textfsm
from lxml import etree
from netaddr import EUI
from netaddr import IPAddress
from netaddr import mac_unix
from netutils.config.parser import IOSConfigParser

try:
    from ttp import quick_parse as ttp_quick_parse

    TTP_INSTALLED = True
except ImportError:
    TTP_INSTALLED = False

# local modules
import napalm.base.exceptions
from napalm.base import constants
from napalm.base.test.models import ConfigDict
from napalm.base.utils.jinja_filters import CustomJinjaFilters
from napalm.base.canonical_map import base_interfaces, reverse_mapping

T = TypeVar("T")
R = TypeVar("R")

# -------------------------------------------------------------------
# Functional Global
# -------------------------------------------------------------------
logger = logging.getLogger(__name__)


# -------------------------------------------------------------------
# helper classes -- will not be exported
# -------------------------------------------------------------------
class _MACFormat(mac_unix):
    pass


_MACFormat.word_fmt = "%.2X"


# -------------------------------------------------------------------
# callable helpers
# -------------------------------------------------------------------
def load_template(
    cls: "napalm.base.NetworkDriver",
    template_name: str,
    template_source: Optional[str] = None,
    template_path: Optional[str] = None,
    openconfig: bool = False,
    jinja_filters: Dict = {},
    **template_vars: Any,
) -> None:
    try:
        search_path = []
        if isinstance(template_source, str):
            template = jinja2.Template(template_source)
        else:
            if template_path is not None:
                if (
                    isinstance(template_path, str)
                    and os.path.isdir(template_path)
                    and os.path.isabs(template_path)
                ):
                    # append driver name at the end of the custom path
                    search_path.append(
                        os.path.join(template_path, cls.__module__.split(".")[-1])
                    )
                else:
                    raise IOError(
                        "Template path does not exist: {}".format(template_path)
                    )
            else:
                # Search modules for template paths
                for c in cls.__class__.mro():
                    if c is object:
                        continue
                    module = sys.modules[c.__module__].__file__
                    if module:
                        path = os.path.abspath(module)
                    else:
                        continue
                    if path:
                        path_to_append = os.path.dirname(path)
                    else:
                        continue
                    if path_to_append:
                        search_path.append(path_to_append)

            if openconfig:
                search_path = ["{}/oc_templates".format(s) for s in search_path]
            else:
                search_path = ["{}/templates".format(s) for s in search_path]

            loader = jinja2.FileSystemLoader(search_path)
            environment = jinja2.Environment(loader=loader)

            for filter_name, filter_function in itertools.chain(
                CustomJinjaFilters.filters().items(), jinja_filters.items()
            ):
                environment.filters[filter_name] = filter_function

            template = environment.get_template(
                "{template_name}.j2".format(template_name=template_name)
            )
        configuration = template.render(**template_vars)
    except jinja2.exceptions.TemplateNotFound:
        raise napalm.base.exceptions.TemplateNotImplemented(
            "Config template {template_name}.j2 not found in search path: {sp}".format(
                template_name=template_name, sp=search_path
            )
        )
    except (
        jinja2.exceptions.UndefinedError,
        jinja2.exceptions.TemplateSyntaxError,
    ) as jinjaerr:
        raise napalm.base.exceptions.TemplateRenderException(
            "Unable to render the Jinja config template {template_name}: {error}".format(
                template_name=template_name, error=str(jinjaerr)
            )
        )
    return cls.load_merge_candidate(config=configuration)


def netutils_parse_parents(
    parent: str, child: str, config: Union[str, List[str]]
) -> List[str]:
    """
    Use Netutils to find parent lines that contain a specific child line.

    :param parent: The parent line to search for
    :param child:  The child line required under the given parent
    :param config: The device running/startup config
    """
    # Check if the config is a list, if it is a list, then join it to make a string.
    if isinstance(config, list):
        config = "\n".join(config)
        config = config + "\n"

    # Config tree is the entire configuration in a tree format,
    # followed by getting the individual lines that has the formats:
    # ConfigLine(config_line=' ip address 192.0.2.10 255.255.255.0',
    # parents=('interface GigabitEthernet1',))
    # ConfigLine(config_line='Current configuration : 1624 bytes', parents=())
    config_tree = IOSConfigParser(str(config))
    configuration_lines = config_tree.build_config_relationship()

    # Return config is the list that will be returned
    return_config = []

    # Loop over each of the configuration lines
    for line in configuration_lines:
        # Loop over any line that has a parent line. If there are no parents for a line item then
        # the parents is an empty tuple.
        for parent_line in line.parents:
            if (
                child in line.config_line
                and re.match(parent, parent_line) is not None
                and parent_line not in return_config
            ):
                return_config.append(parent_line)

    return return_config


def netutils_parse_objects(
    cfg_section: str, config: Union[str, List[str]]
) -> List[str]:
    """
    Use Netutils to find and return a section of Cisco IOS config.
    Similar to "show run | section <cfg_section>"

    :param cfg_section: The section of the config to return eg. "router bgp"
    :param config: The running/startup config of the device to parse
    """
    # Check if the config is a list, if it is a list, then join it to make a string.
    if isinstance(config, list):
        config = "\n".join(config)
        config = config + "\n"

    # Config tree is the entire configuration in a tree format,
    # followed by getting the individual lines that has the formats:
    # ConfigLine(config_line=' ip address 192.0.2.10 255.255.255.0',
    # parents=('interface GigabitEthernet1',))
    # ConfigLine(config_line='Current configuration : 1624 bytes', parents=())
    config_tree = IOSConfigParser(str(config))
    lines = config_tree.build_config_relationship()

    # Return config is the list that will be returned
    return_config = []
    for line in lines:
        # The parent configuration is expected on the function that this is replacing,
        # add the parent line to the base of the return_config
        if cfg_section in line.config_line:
            return_config.append(line.config_line)
        # Check if the tuple is greater than 0
        if len(line.parents) > 0:
            # Check the eldest parent, if that is part of the config section, then append
            # the current line being checked to it.
            if cfg_section in line.parents[0]:
                return_config.append(line.config_line)

    return return_config


def regex_find_txt(pattern: str, text: str, default: str = "") -> Any:
    """ ""
    RegEx search for pattern in text. Will try to match the data type of the "default" value
    or return the default value if no match is found.
    This is to parse IOS config like below:
    regex_find_txt(r"remote-as (65000)", "neighbor 10.0.0.1 remote-as 65000", default=0)
    RETURNS: 65001

    :param pattern: RegEx pattern to match on
    :param text: String of text ot search for "pattern" in
    :param default="": Default value and type to return on error
    """
    text = str(text)
    value = re.findall(pattern, text)
    try:
        if not value:
            logger.error("No Regex match found for pattern: %s" % (str(pattern)))
            raise Exception("No Regex match found for pattern: %s" % (str(pattern)))
        if not isinstance(value, type(default)):
            if isinstance(value, list) and len(value) == 1:
                value = value[0]
            value = type(default)(value)  # type: ignore
    except Exception as regexFindTxtErr01:  # in case of any exception, returns default
        logger.error(