"""Provides a set of functions nad clases for different purpose."""

import logging
from logging.handlers import WatchedFileHandler
import socket
import codecs
import time
import re
import os
import yaml


def delegate(attribute_name, method_names):
    """Pass the call to the attribute called attribute_name for every method listed in method_names."""
    # hack for python 2.7 as nonlocal is not available
    info = {
        'attribute': attribute_name,
        'methods': method_names
    }

    def decorator(cls):
        """Decorate class."""
        attribute = info['attribute']
        if attribute.startswith("__"):
            attribute = "_" + cls.__name__ + attribute
        for name in info['methods']:
            setattr(cls, name, eval("lambda self, *a, **kw: "
                                    "self.{0}.{1}(*a, **kw)".format(attribute, name)))
        return cls
    return decorator


def to_list(item):
    """Convert to list.

    If the given item is iterable, this function returns the given item.
    If the item is not iterable, this function returns a list with only the
    item in it.

    @type  item: object
    @param item: Any object.
    @rtype:  list
    @return: A list with the item in it.
    """
    if hasattr(item, '__iter__'):
        return item
    return [item]


def is_reachable(host, port=23):
    """Check reachability for specified hostname/port.

    It tries to open TCP socket.
    It supports IPv6.
    :param host: hostname or ip address string
    :rtype: str
    :param port: tcp port number
    :rtype: number
    :return: True if host is reachable else false
    """
    try:
        addresses = socket.getaddrinfo(
            host, port, socket.AF_UNSPEC, socket.SOCK_STREAM
        )
    except socket.gaierror:
        return False

    for family, _, _, _, sockaddr in addresses:
        sock = socket.socket(family, socket.SOCK_STREAM)
        sock.settimeout(5)
        try:
            sock.connect(sockaddr)
        except IOError:
            continue

        sock.shutdown(socket.SHUT_RDWR)
        sock.close()
        # Wait 2 sec for socket to shutdown
        time.sleep(2)
        break
    else:
        return False
    return True


def pattern_to_str(pattern):
    """Convert regex pattern to string.

    If pattern is string it returns itself,
    if pattern is SRE_Pattern then return pattern attribute
    :param pattern: pattern object or string
    :return: str: pattern sttring
    """
    if isinstance(pattern, str):
        return repr(pattern)
    else:
        return repr(pattern.pattern) if pattern else None


def levenshtein_distance(str_a, str_b):
    """Calculate the Levenshtein distance between string a and b.

    :param str_a: String - input string a
    :param str_b: String - input string b
    :return: Number - Levenshtein Distance between string a and b
    """
    len_a, len_b = len(str_a), len(str_b)
    if len_a > len_b:
        str_a, str_b = str_b, str_a
        len_a, len_b = len_b, len_a
    current = range(len_a + 1)
    for i in range(1, len_b + 1):
        previous, current = current, [i] + [0] * len_a
        for j in range(1, len_a + 1):
            add, delete = previous[j] + 1, current[j - 1] + 1
            change = previous[j - 1]
            if str_a[j - 1] != str_b[i - 1]:
                change += + 1
            current[j] = min(add, delete, change)
    return current[len_a]


def parse_inventory(inventory_output=None):
    """Parse the inventory text and return udi dict."""
    udi = {
        "name": "",
        "description": "",
        "pid": "",
        "vid": "",
        "sn": ""
    }
    if inventory_output is None:
        return udi

    # find the record with chassis text in name or descr
    capture_next = False
    chassis_udi_text = None
    for line in inventory_output.split('\n'):
        lc_line = line.lower()
        if 'chassis' in lc_line and 'name' in lc_line and 'descr':
            capture_next = True
            chassis_udi_text = line
            continue
        if capture_next:
            inventory_output = chassis_udi_text + "\n" + line
            break

    match = re.search(r"(?i)NAME: (?P<name>.*?),? (?i)DESCR", inventory_output, re.MULTILINE)
    if match:
        udi['name'] = match.group('name').strip('" ,')

    match = re.search(r"(?i)DESCR: (?P<description>.*)", inventory_output, re.MULTILINE)
    if match:
        udi['description'] = match.group('description').strip('" ')

    match = re.search(r"(?i)PID: (?P<pid>.*?),? ", inventory_output, re.MULTILINE)
    if match:
        udi['pid'] = match.group('pid')

    match = re.search(r"(?i)VID: (?P<vid>.*?),? ", inventory_output, re.MULTILINE)
    if match:
        udi['vid'] = match.group('vid')

    match = re.search(r"(?i)SN: (?P<sn>.*)", inventory_output, re.MULTILINE)
    if match:
        udi['sn'] = match.group('sn')
    return udi


class FilteredFile(object):
    """Delegate class for handling filtered file object."""

    __slots__ = ['_file', '_pattern']

    def __init__(self, filename, mode="r", encoding=None, pattern=None):
        """Initialize FilteredFile object."""
        object.__setattr__(self, '_pattern', pattern)
        if encoding is None:
            object.__setattr__(self, '_file', open(filename, mode=mode))
        else:
            object.__setattr__(self, '_file', codecs.open(filename, mode=mode, encoding=encoding))

    def __getattr__(self, name):
        """Override standard getattr and delegate to file object."""
        return getattr(self._file, name)

    def __setattr__(self, name, value):
        """Override standard setattr and delegate to file object."""
        setattr(self._file, name, value)

    def write(self, text):
        """Override the standard write method to filter the content."""
        if self._pattern:
            # pattern already compiled no need to check
            result = re.search(self._pattern, text)
            if result:
                for group in result.groups():
                    if group:
                        text = text.replace(group, "***")
        self._file.write(text)

    def __exit__(self, exc_type, exc_val, exc_tb):
        """Custom context manager exit method."""
        self._file.close()

    def __enter__(self):
        """Custom context manager exit method."""
        return self


class FilteredFileHandler(WatchedFileHandler):
    """Class defining custom FileHandler for filtering sensitive information."""

    def __init__(self, filename, mode='a', encoding="utf-8", delay=0, pattern=None):
        """Initialize the FilteredFileHandler object."""
        self.pattern = pattern
        WatchedFileHandler.__init__(self, filename, mode=mode, encoding=encoding, delay=delay)

    def _open(self):
        return FilteredFile(self.baseFilename, mode=self.mode, encoding=self.encoding, pattern=self.pattern)


def normalize_urls(urls):
    """Overload urls and make list of lists of urls."""
    _urls = []
    if isinstance(urls, list):
        if urls:
            if isinstance(urls[0], list):
                # multiple connections (list of the lists)
                _urls = urls
            elif isinstance(urls[0], str):
                # single connections (make it list of the lists)
                _urls = [urls]
        else:
            raise RuntimeError("No target host url provided.")
    elif isinstance(urls, str):
        _urls = [[urls]]
    return _urls


def make_handler(log_dir, log_level):
    """Make logging handler."""
    if log_level > 0:
        if log_level == logging.DEBUG:
            formatter = logging.Formatter('%(asctime)-15s [%(levelname)8s] %(name)s:'
                                          '%(funcName)s(%(lineno)d): %(message)s')
        else:
            formatter = logging.Formatter('%(asctime)-15s [%(levelname)8s]: %(message)s')
        if log_dir:
            # Create the log directory.
            if not os.path.exists(log_dir):
                try:
                    os.makedirs(log_dir)
                except IOError:
                    log_dir = "./"
            log_filename = os.path.join(log_dir, 'condoor.log')
            # FIXME: take pattern from pattern manager
            handler = FilteredFileHandler(log_filename, pattern=re.compile("s?ftp://.*:(.*)@"))

        else:
            handler = logging.StreamHandler()

        handler.setFormatter(formatter)
    else:
        handler = logging.NullHandler()

    return handler


def yaml_file_to_dict(script_name, path=None):
    """Read yaml file and return the dict.

    It assumes the module file exists with the defaults.
    If the CONDOOR_{SCRIPT_NAME} env is set then the user file from the env is loaded and merged with the default

    There can be user file located in ~/.condoor directory with the {script_name}.yaml filename. If exists
    it is merget with default config.
    """
    def load_yaml(file_path):
        """Load YAML file from full file path and return dict."""
        with open(file_path, 'r') as yamlfile:
            try:
                dictionary = yaml.load(yamlfile)
            except yaml.YAMLError:
                return {}
        return dictionary

    def merge(user, default):
        """Merge two dicts."""
        if isinstance(user, dict) and isinstance(default, dict):
            for k, v in default.iteritems():
                if k not in user:
                    user[k] = v
                else:
                    user[k] = merge(user[k], v)
            return user
        else:
            return default

    if path is None:
        path = os.path.abspath('.')

    config_file_path = os.path.join(path, script_name + '.yaml')
    if not os.path.exists(config_file_path):
        raise RuntimeError('Config file does not exist: {}'.format(config_file_path))

    default_dict = load_yaml(config_file_path)

    user_config_file_path = os.path.join(os.path.expanduser('~'), '.condoor', script_name + '.yaml')
    user_config_file_path = os.getenv('CONDOOR_' + script_name.upper(), user_config_file_path)

    if os.path.exists(user_config_file_path):
        user_dict = load_yaml(user_config_file_path)
        default_dict = merge(user_dict, default_dict)

    return default_dict
