"""Module containing helper functions that are used by other parts of worker."""

import datetime
import getpass
import json
import logging
import signal
import re
from contextlib import contextmanager
from os import path as os_path, walk, getcwd, chdir, environ as os_environ, killpg, getpgid
from queue import Queue, Empty
from shlex import split
from subprocess import Popen, PIPE, check_output, CalledProcessError, TimeoutExpired
from threading import Thread
from traceback import format_exc
from urllib.parse import unquote, urlparse, parse_qs
import tenacity

import requests
from requests.adapters import HTTPAdapter
from requests.exceptions import HTTPError
from requests.packages.urllib3.util.retry import Retry
from selinon import StoragePool
from sqlalchemy.exc import SQLAlchemyError

from f8a_worker.enums import EcosystemBackend
from f8a_worker.errors import (TaskError,
                               NotABugTaskError,
                               F8AConfigurationException)
from f8a_worker.models import (Analysis,
                               Ecosystem,
                               Package,
                               Version)
from f8a_worker.defaults import configuration

logger = logging.getLogger(__name__)


def get_latest_analysis(ecosystem, package, version, db_session=None):
    """Get latest analysis for the given EPV."""
    if not db_session:
        storage = StoragePool.get_connected_storage("BayesianPostgres")
        db_session = storage.session

    try:
        return db_session.query(Analysis). \
            filter(Ecosystem.name == ecosystem). \
            filter(Package.name == package). \
            filter(Version.identifier == version). \
            order_by(Analysis.started_at.desc()). \
            first()
    except SQLAlchemyError:
        db_session.rollback()
        raise


@contextmanager
def cwd(target):
    """Manage cwd in a pushd/popd fashion."""
    curdir = getcwd()
    chdir(target)
    try:
        yield
    finally:
        chdir(curdir)


@contextmanager
def username():
    """Workaround for failing getpass.getuser().

    http://blog.dscpl.com.au/2015/12/unknown-user-when-running-docker.html
    """
    user = ''
    try:
        user = getpass.getuser()
    except KeyError
:
        os_environ['LOGNAME'] = 'f8aworker'

    try:
        yield
    finally:
        if not user:
            del os_environ['LOGNAME']


def assert_not_none(name, value):
    """Assert value is not None."""
    if value is None:
        raise ValueError('Parameter %r is None' % name)


class TimedCommand(object):
    """Exe
cute arbitrary shell command in a timeout-able manner."""

    def __init__(self, command):
        """Initialize command."""
        # parse with shlex if not execve friendly
        if isinstance(command, str):
            command = split(command)

        self.command = command

    def run(self, timeout=None, is_json=False, **kwargs):
        """Run the self.command and wait up to given time period for results.

        :param timeout: how long to wait, in seconds, for the command to finish
        before terminating it
        :param is_json: hint whether output of the command is a JSON
        :return: triplet (return code, stdout, stderr), stdout will be a
        dictionary if `is_json` is True
        """
        logger.debug("running command '%s'; timeout '%s'", self.command, timeout)

        # this gets executed in a separate thread
        def target(**kwargs):
            try:
                self.process = Popen(self.command, universal_newlines=True, **kwargs)
                self.output, self.error = self.process.communicate()
                self.status = self.process.returncode
            except Exception:
                self.output = {} if is_json else []
                self.error = format_exc()
                self.status = -1

        # default stdout and stderr
        if 'stdout' not in kwargs:
            kwargs['stdout'] = PIPE
        if 'stderr' not in kwargs:
            kwargs['stderr'] = PIPE
        if 'update_env' in kwargs:
            # make sure we update environment, not override it
            kwargs['env'] = dict(os_environ, **kwargs['update_env'])
            kwargs.pop('update_env')

        # thread
        thread = Thread(target=target, kwargs=kwargs)
        thread.start()
        thread.join(timeout)

        # timeout reached, terminate the thread
        if thread.is_alive():
            logger.error('Command {cmd} timed out after {t} seconds'.format(cmd=self.command,
                                                                            t=timeout))
            # this is tricky - we need to make sure we kill the process with all its subprocesses;
            #  using just kill might create zombie process waiting for subprocesses to finish
            #  and leaving us hanging on thread.join()
            # TODO: we should do the same for get_command_output!
            killpg(getpgid(self.process.pid), signal.SIGKILL)
            thread.join()
            if not self.error:
                self.error = 'Killed by timeout after {t} seconds'.format(t=timeout)
        if self.output:
            if is_json:
                self.output = json.loads(self.output)
            else:
                self.output = [f for f in self.output.split('\n') if f]

        return self.status, self.output, self.error

    @staticmethod
    def get_command_output(args, graceful=True, is_json=False, timeout=300, **kwargs):
        """Wrap the function to get command output with implicit timeout of 5 minutes."""
        kwargs['timeout'] = 10800
        return get_command_output(args, graceful, is_json, **kwargs)


def get_command_output(args, graceful=True, is_json=False, **kwargs):
    """Improved version of subprocess.check_output.

    :param graceful: bool, if False, raise Exception when command fails
    :param is_json: bool, if True, return decoded json

    :return: list of strings, output which command emitted
    """
    logger.debug("running command %s", args)
    try:
        # Using universal_newlines mostly for the side-effect of decoding
        # the output as UTF-8 text on Python 3.x
        out = check_output(args, universal_newlines=True, **kwargs)
    except (CalledProcessError, TimeoutExpired) as ex:
        # TODO: we may want to use subprocess.Popen to be able to also print stderr here
        #  (while not mixing it with stdout that is returned if the subprocess succeeds)
        if isinstance(ex, TimeoutExpired):
            logger.warning("command %s timed out:\n%s", args, ex.output)
        else:
            logger.warning("command %s ended with %s\n%s", args, ex.returncode, ex.output)

        if not graceful:
            logger.error(ex)
            # we don't know whether this is a bug or the command was simply called
            # with invalid/unsupported input. Caller needs to catch the exception
            # and decide.
            raise TaskError("Error during running command %s: %r" % (args, ex.output))
        else:
            logger.debug("Ignoring because graceful flag is set")
        return []
    else:
        if is_json:
            # FIXME: some error handling here would be great
            return json.loads(out)
        else:
            return [f for f in out.split('\n') if f]  # py2 & 3 compat


def get_all_files_from(target, path_filter=None, file_filter=None):
    """Enumerate all files in target directory, can be filtered with custom delegates."""
    for root, dirs, files in walk(target):
        for file in files:
            joined = os_path.abspath(os_path.join(root, file))

            # filter the list early on
            if path_filter and not path_filter(joined):
                continue

            if file_filter and not file_filter(file):
                continue

            yield joined


def hidden_path_filter(item):
    """Filter out hidden files or files in hidden directories."""
    return not any(sub.startswith('.') for sub in item.split(os_path.sep))


def json_serial(obj):
    """Return time obj formatted according to ISO."""
    if isinstance(obj, datetime.datetime):
        r