import copy
import errno
import inspect
import logging
import logging.handlers
import os
import requests
import socket
from six import iteritems
import sys
import traceback
from difflib import SequenceMatcher
from gymail.core import send_mail
from subprocess import Popen, PIPE


def _wrap_with(code):

    def inner(text, bold=False):
        c = code
        if bold:
            c = "1;%s" % c
        return "\033[%sm%s\033[0m" % (c, text)
    return inner


tor_proxies = {
    'http': 'socks5://127.0.0.1:9050',
    'https': 'socks5://127.0.0.1:9050'
}
red, green, yellow = _wrap_with('31'), _wrap_with('32'), _wrap_with('33')
blue, magenta, cyan, blue = _wrap_with('34'), _wrap_with('35'), _wrap_with('36'), _wrap_with('37')


class SetupLogger():
    """Logs to syslog and console with some format conveniences. Instantiating class here, because other functions in
       here are dependent on this logger. So import the instance log from here and don't create a new instance of
       SetupLogger unless you want to derive from it."""

    def __init__(self, verbose):
        self.logger = logging.getLogger()  # Initializing Logger
        syslog_handler = logging.handlers.SysLogHandler('/dev/log')
        self.logger.addHandler(syslog_handler)  # Add SysLogHandler
        self.console_handler = logging.StreamHandler()
        self.logger.addHandler(self.console_handler)
        if verbose == "vvv":
            self.logger.setLevel(logging.DEBUG)
        elif verbose == "vv":
            self.logger.setLevel(logging.INFO)
        elif verbose == "v":
            self.logger.setLevel(logging.ERROR)

    def info(self, message, newline=0):
        """Automatically log the current function details."""
        if newline > 0:
            self.newline(newline)
        self.logger.info("(I) {0}".format(message))

    def error(self, message):
        func = inspect.currentframe().f_back.f_code
        self.logger.error("{0}(E) {1}".format(func.co_name, red(message)))

    def warning(self, message):
        func = inspect.currentframe().f_back.f_code
        self.logger.warning("{0}(W) {1}".format(func.co_name, (message)))
        self.logger.warning("{0}".format(cyan(message)))

    def debug(self, message):
        func = inspect.currentframe().f_back.f_code
        self.logger.debug("{0}(D) {1}".format(func.co_name, magenta(message)))

    def newline(self, how_many_lines=1):
        for i in range(how_many_lines):
            self.logger.info('')


log = SetupLogger('vvv')


def _isfile(f, hn=False):
    """Checks if file is a file, either locally or remotely."""
    cmd = ["file", "-b", f]
    if hn and not islocal(hn):
        cmd = ["ssh", hn] + cmd
    p1 = Popen(cmd, stdout=PIPE)
    out = p1.communicate()[0].decode("UTF-8")
    if "No such file" in out:
        return None
    elif "directory" in out:
        return False
    else:
        return True


def islocal(hn1):
    """Checks if the hostname is the local hostname"""
    hn2 = socket.gethostname()
    log.debug("hn1: %s hn2: %s" % (hn1, hn2))
    if hn1 in hn2 or hn2 in hn1:
        return True
    else:
        return False


def rsync(src, dst, remote_host=None, exclude=list(), tor=False):
    """Rsync wrapper with exclude and remote_host"""
    src = os.path.normpath(src)
    torsocks = ["torsocks"] if tor else []
    if remote_host:
        p1 = Popen(torsocks + ["rsync", "-avHAXx"] + exclude + [src, "{0}:{1}".format(remote_host, dst)], stdout=PIPE)
    else:
        p1 = Popen(["rsync", "-avHAXx"] + exclude + [src, dst], stdout=PIPE)
    log.info(p1.communicate())
    log.info(p1.returncode)
    if p1.returncode != 0:
        log.error("Rsync error. sendmail")


def is_number(s):
    """Checks if given parameter is a number or not"""
    try:
        float(s)
        return True
    except ValueError:
        return False


def find_mountpoint(path, hn=False):
    """Returns mountpoint from local or given remote system (requires ssh alias for hostname to be configured)."""
    cmd = ["findmnt", "-T", path, "-n", "-o", "TARGET"]
    log.debug("hn: %s, path %s" % (hn, path))
    if hn and not islocal(hn):
        cmd = ["ssh", hn] + cmd
        log.debug("cmd: %s" % str(cmd))
    p1 = Popen(cmd, stdout=PIPE, stderr=PIPE)
    path, err = p1.communicate()
    path = path.decode("UTF-8").strip()
    log.debug("err: %s" % err)
    log.info("mountpoint is: %s" % path)
    return path


def format_exception(e):
    """Usage:  except Exception as e: log.error(format_exception(e))"""
    exception_list = traceback.format_stack()
    exception_list = exception_list[:-2]
    exception_list.extend(traceback.format_tb(sys.exc_info()[2]))
    exception_list.extend(traceback.format_exception_only(sys.exc_info()[0], sys.exc_info()[1]))
    exception_str = "Traceback (most recent call last):\n"
    exception_str += "".join(exception_list)
    exception_str = exception_str[:-1]  # Removing the last \n
    return exception_str


def listdir_fullpath(d):
    """List dirs in given director with their fullpath."""
    return [os.path.join(d, f) for f in os.listdir(d)]


def remote_file_content(hn, fn):
    """Returns files content UTF-8 decoded via ssh by the given hostname and filename."""
    log.info("content of %s:%s (hn:filename)" % (hn, fn))
    p1 = Popen(["ssh", hn, "sudo", "/usr/bin/cat", fn], stdout=PIPE)
    return p1.communicate()[0].decode("UTF-8").split()


def try_func(func, *args, **kwargs):
    """Try except wrapper."""
    try:
        val = func(*args, **kwargs)
        return val
    except Exception as e:
        log.error(format_exception(e))
        return False


def similar(a, b):
    """Returns float from 0 to 1 of how similar two strings are."""
    return SequenceMatcher(None, a, b).ratio()


def mkdir_p(path):
    try:
        os.makedirs(path)
    except OSError as exc:
        if exc.errno == errno.EEXIST and os.path.isdir(path):
            pass
        else:
            log.error("Could not create directory")
            raise


def umount(mp, lazy=False, fuser=False):
    """Umounts given mountpoint. Can umount also with lazy switch."""
    cmd_umount, cmd_lazy_umount = (["/usr/bin/umount"], ["/usr/bin/umount", "-l"])
    if fuser:
        cmd_umount, cmd_lazy_umount = (["/usr/bin/fusermount", "-u"], ["/usr/bin/fusermount", "-u", "-z"])
    cmd = cmd_lazy_umount if lazy else cmd_umount
    log.debug(cmd)
    p1 = Popen(cmd + [mp], stdout=PIPE, stderr=PIPE)
    out, err = p1.communicate()
    log.info("umount: out: %s, err: %s" % (out, err))
    log.info("Unmounted mp %s" % mp)


def mount(dev, mp):
    """Mounts a dev to mountpoint. If mp is occupied it'll try to umount first."""
    if os.path.ismount(mp):
        log.warning("Already mounted. Trying to unmount.")
        umount(mp)
        if os.path.ismount(mp):
            log.warning("Still mounted. Trying lazy unmount.")
            try_func(umount, mp, lazy=True)
            if os.path.ismount(mp):
                log.error("Couldn't be unmounted.")
                return False
    cmd_mount = ["mount", dev, mp]
    try:
        p1 = Popen(cmd_mount, stdin=PIPE)
        out, err = p1.communicate()
        log.debug("out: %s, err: %s" % (out, err))
        if p1.returncode == 0 and os.path.ismount(mp):
            log.info("Mounted %s to %s" % (dev, mp))
            return True
        else:
            log.error("Mounting failed. Error: %s\n%s" % (out, err))
            return False
    except Exception as e:
        log.error(format_exception(e))
        return False


def systemd_services_up(services):
    """Checks if given systems services are up, if not it exists."""
    for x in services:
        p = Popen(["systemctl", "is-active", x], stdout=PIPE, stderr=PIPE)
        out, err = p.communicate()
        out = out.decode("utf-8").strip()
        if "failed" == out:
            log.info("Exiting, because dependent services are down: %s" % services)
            sys.exit()


def my_ip(proxies=None):
    kwargs = { "proxies": proxies } if proxies else {}
    url = 'http://myip.dnsomatic.com'
    r = requests.get(url, **kwargs)
    return r.text


def download(filename, download_link, proxies=None, tor=False):
    """Download file from given download link to given local filename path."""
    with open(filename, 'wb') as f:
        kwargs = {}
        if tor:
            log.debug("requests with tor enabled, using requests >=2.10 with socks5 support.")
            log.debug("Current IP: %s" % my_ip(proxies=tor_proxies))
            kwargs = { "proxies": tor_proxies }
        elif proxies:
            log.debug("Using custom proxies.")
            log.debug("Current IP: %s" % my_ip(proxies=proxies))
            kwargs = { "proxies": proxies }
        response = requests.get(download_link, stream=True, **kwargs)
        if not response.ok:
            send_mail(event="error", subject=os.path.basename(__file__),
                      message="Download from %s failed. Site possibly down." % download_link)
        for block in response.iter_content(1024):
            f.write(block)


def newest_dir(b='.'):
    """Returns newest (=last modified) directory of given path."""
    all_dirs = []
    for d in os.listdir(b):
        bd = os.path.join(b, d)
        if os.path.isdir(bd): all_dirs.append(bd)
    newest_dir = max(all_dirs, key=os.path.getmtime)
    return newest_dir


def handle_exceptions(fn):
    """Decorator for functions/methods to catch exceptions, apply with @handle_exception above method."""
    from functools import wraps
    @wraps(fn)
    def wrapper(self, *args, **kw):
        try:
            return fn(self, *args, **kw)
        except Exception as e:
            log.error("Catched error with handle_exception decorator")
            log.error(format_exception(e))
    return wrapper


def multilog(**kwargs):
    """Convenience method, which prints the formated kwargs to log.debug()."""
    merged = str()
    for k, v in iteritems(kwargs):
        if len(v) > 80:
            m = "\n{0}: {1},\n".format(k, v)
        else:
            m = "{0}: {1}, ".format(k, v)
        merged += m
    log.debug(merged)
