import posixpath
import re
import sys
from pipes import quote

from parallels.core import messages, MigrationError, safe_utf8_encode
from parallels.core import safe_format
from parallels.core.logging import get_logger
from parallels.core.runners.entities import ExecutionOptions
from parallels.core.utils.common import is_empty

logger = get_logger(__name__)


def is_debian(runner):
    """
    :type runner: parallels.core.runners.base.BaseRunner 
    :rtype: bool 
    """
    return runner.file_exists('/etc/debian_version')


def is_centos(runner):
    """
    :type runner: parallels.core.runners.base.BaseRunner 
    :rtype: bool 
    """
    return runner.file_exists('/etc/redhat-release')


def is_suse(runner):
    """
    :type runner: parallels.core.runners.base.BaseRunner 
    :rtype: bool 
    """
    return runner.file_exists('/etc/SuSE-release')


def map_copy_owner_user(runner_source, runner_target, source_basedir, target_basedir, owner_from, owner_to):
    """For each file on source node located in 'source_basedir' and owned by user 'owner_from' 
    change user to 'owner_to' on target node in 'target_basedir'"""
    _map_copy_owner(
        runner_source, runner_target, source_basedir, target_basedir, owner_from, owner_to, "user", "/bin/chown"
    )


def map_copy_owner_group(runner_source, runner_target, source_basedir, target_basedir, owner_from, owner_to):
    """For each file on source node located in 'source_basedir' and owned by group 'owner_from' 
    change group to 'owner_to' on target node in 'target_basedir'"""
    _map_copy_owner(
        runner_source, runner_target, source_basedir, target_basedir, owner_from, owner_to, "group", "/bin/chgrp"
    )


def _map_copy_owner(
    runner_source, runner_target, source_basedir, target_basedir, owner_from, owner_to, find_criteria, change_command
):
    """
    :type runner_source: parallels.core.runners.base.BaseRunner
    :type runner_target: parallels.core.runners.base.BaseRunner
    """
    filenames = [
        filename for filename in runner_source.execute_command(
            "/usr/bin/find {source_basedir} -{find_criteria} {owner_from} -printf '%P\\0'",
            dict(source_basedir=source_basedir, find_criteria=find_criteria, owner_from=owner_from)
        ).stdout.split('\0')
        if filename != ''
    ]
    for filename in filenames:
        target_filename = posixpath.join(target_basedir, filename)
        if runner_target.file_exists(target_filename):
            runner_target.execute_command(
                "{change_command} {owner_to} {target_filename}",
                dict(
                    change_command=change_command, owner_to=owner_to, target_filename=target_filename
                )
            )


def format_command(template, *args, **kw):
    qargs = map(quote, args)
    if sys.version_info.major >= 3:
        qkw = dict((k, quote(v)) for k, v in kw.items())
    else:
        qkw = dict((k, quote(safe_utf8_encode(v))) for k, v in kw.items())
    try:
        if sys.version_info.major >= 3:
            return template.format(*qargs, **qkw)
        else:
            return safe_utf8_encode(template).format(*qargs, **qkw)
    except KeyError as e:
        if not is_empty(e.args[0]):
            logger.exception()
            raise MigrationError(safe_format(
                messages.FAILED_TO_FORMAT_COMMAND,
                command=template, missing_argument=e.args[0]
            ))
        else:
            raise


def format_command_list(cmd, args):
    if sys.version_info.major >= 3:
        bytes_type = bytes
    else:
        bytes_type = basestring
    return " ".join([
        quote(arg if isinstance(arg, bytes_type) else str(arg))
        for arg in [cmd] + ([] if args is None else args)
    ])


def get_files_list_with_find(runner, path):
    """
    :type runner: parallels.core.runners.base.BaseRunner
    :type path: str | unicode
    """
    files = runner.execute_command(
        'find {path} -maxdepth 1 -type f -printf "%f\\0"', dict(path=path),
        # ignore exit code for situations when we are not able to read directory listing, for example
        # if we don't have enough permissions
        execution_options=ExecutionOptions(ignore_exit_code=True, log_output=False)
    ).stdout.split("\0")
    return [item for item in files if item]


def get_files_list_with_ls(runner, path):
    """
    :type runner: parallels.core.runners.base.BaseRunner
    :type path: str | unicode
    """
    return [
        line
        for line in runner.execute_command(
            'cd {path} && ls -p', dict(path=path),
            ExecutionOptions(ignore_exit_code=True, log_output=False)
        ).stdout.split("\n")
        if line != ''
    ]


def get_directories_list_with_find(runner, path):
    """
    :type runner: parallels.core.runners.base.BaseRunner
    :type path: str | unicode
    :rtype: list[str | unicode]
    """
    files = runner.execute_command(
        'find {path} -mindepth 1 -maxdepth 1 -type d -printf "%f\\0"', dict(path=path),
        # ignore exit code for situations when we are not able to read directory listing, for example
        # if we don't have enough permissions
        execution_options=ExecutionOptions(ignore_exit_code=True, log_output=False)
    ).stdout.split("\0")
    return [item for item in files if item]


def get_directories_list_with_ls(runner, path):
    """
    :type runner: parallels.core.runners.base.BaseRunner
    :type path: str | unicode
    :rtype: list[str | unicode]
    """
    return [
        line.rstrip('/')
        for line in runner.execute_command(
            'cd {path} && ls -1 -d */ .*/', dict(path=path),
            ExecutionOptions(ignore_exit_code=True, log_output=False)
        ).stdout.split("\n")
        if line not in ('../', './', '')
    ]


def get_dir_tree_with_find(runner, path):
    """Retrieve tree structure of the directory with given path using "find" utility, like the following:

    [
        'file_1',
        'file_2',
        {
            'dir_1': [
                'file_3'
            ]
        }
    ]

    :type runner: parallels.core.runners.base.BaseRunner
    :type path: str | unicode
    :rtype: list[str | {str, list}]
    """
    file_info = runner.execute_command(
        'find {path} -mindepth 1 -printf "%y\\0%P\\0"', dict(path=path),
        # ignore exit code for situations when we are not able to read directory listing, for example
        # if we don't have enough permissions
        execution_options=ExecutionOptions(ignore_exit_code=True, log_output=False)
    ).stdout.split("\0")

    # Compose directory tree in "dictionary" format, where each directory is a dictionary where:
    # - key is name of a file or a directory,
    # - value is None if that is a file
    # - value is dictionary if that is a directory
    # For example:
    # {
    #   'httpdocs':
    #   {
    #     'wordpress':
    #       {
    #         'wp-config.php': None,
    #         'wp-content': {}
    #       }
    #   }
    # }
    directory_tree_dict = {}

    # iterate over pairs of (file type, file path) according to find format (see "-printf" option)
    if sys.version_info.major >= 3:
        item_range = range(0, int((len(file_info) - 1) / 2))
    else:
        item_range = xrange(0, (len(file_info) - 1) / 2)
    for i in item_range:
        file_type = file_info[i * 2]
        file_path = file_info[i * 2 + 1]

        if file_type == 'd':
            file_path_parts = file_path.split('/')
            current_item = directory_tree_dict
            for item in file_path_parts:
                if item not in current_item:
                    current_item[item] = {}
                current_item = current_item[item]
        elif file_type == 'f':
            file_path_parts = file_path.split('/')
            directory_path_parts = file_path_parts[:-1]
            file_name = file_path_parts[-1]
            current_item = directory_tree_dict
            for item in directory_path_parts:
                if item not in current_item:
                    current_item[item] = {}
                current_item = current_item[item]
            current_item[file_name] = None
        else:
            # we are not interested in symlinks, sockets, etc
            pass

    # Convert directory tree to "list" format:
    # "list" format is used commonly in migrator code,
    # see parallels.core.runners.base.BaseRunner#get_dir_tree function for usage and details.
    # Each directory in such format is a list, where each item is:
    # - a string if the item is file
    # - a dictionary with a single element if item is dictionary, key is a directory name, value is a list
    # For example:
    # [
    #   {
    #     'httpdocs':
    #     [
    #       {
    #         'wordpress':
    #         [
    #           'wp-config.php',
    #           {'wp-content': []}
    #         ]
    #       }
    #     ]
    #   }
    # ]

    def convert_directory_tree_dict_to_list(tree):
        result = []
        for item_name, item_value in tree.items():
            if item_value is None:
                result.append(item_name)
            elif isinstance(item_value, dict):
                result.append({
                    item_name: convert_directory_tree_dict_to_list(item_value)
                })
        return result

    return convert_directory_tree_dict_to_list(directory_tree_dict)


def get_apache_httpd_conf(runner):
    """
    :type runner: parallels.core.runners.base.BaseRunner
    """
    try:
        # Detect apache httpd.conf. In all systems it can be in different places. Therefore, we use the setting
        # of running apache.
        apache_process = runner.execute_command(
            u'ps -ef | grep "apache2\|httpd" | grep -v grep'
        ).stdout.splitlines()[0]
        httpd = re.search(r'(([^\s]+\s*){8}([^\s]+).*)', apache_process).group(2).strip()
        _server_config_file = runner.execute_command(
            u'{httpd_bin} -V | grep SERVER_CONFIG_FILE', dict(httpd_bin=httpd)
        ).stdout.strip()
        server_config_file = re.search(r'(.*)SERVER_CONFIG_FILE=(.*)', _server_config_file).group(2).strip('"')
        _httpd_root = runner.execute_command(
            u'{httpd_bin} -V | grep HTTPD_ROOT', dict(httpd_bin=httpd)
        ).stdout.strip()
        httpd_root = re.search(r'(.*)HTTPD_ROOT=(.*)', _httpd_root).group(2).strip('"')
        apache_httpd_conf = posixpath.join(httpd_root, server_config_file)
        if not runner.file_exists(apache_httpd_conf):
            return None
        logger.fdebug(messages.DETECTED_APACHE_CONF, file=apache_httpd_conf)
        return apache_httpd_conf
    except:
        return None


def get_apache_user(runner, apache_httpd_conf):
    """
    :type runner: parallels.core.runners.base.BaseRunner
    """
    try:
        if apache_httpd_conf is None:
                return None

        result = runner.execute_command(u"grep -i '^user\s' {httpd_conf}", dict(httpd_conf=apache_httpd_conf)).stdout
        user = re.search(r'[^\s]+\s*([^\s]+).*', result).group(1)
        exit_code = runner.execute_command(
            u"grep '^{user}:' /etc/passwd", dict(user=user),
            ExecutionOptions(ignore_exit_code=True)
        ).exit_code
        if exit_code != 0:
            return None
        logger.fdebug(messages.DETECTED_APACHE_USER, user=user)
        return user
    except:
        return None


def get_apache_group(runner, apache_httpd_conf):
    """
    :type runner: parallels.core.runners.base.BaseRunner
    """
    try:
        if apache_httpd_conf is None:
                return None

        result = runner.execute_command(u"grep -i '^group\s' {httpd_conf}", dict(httpd_conf=apache_httpd_conf)).stdout
        group = re.search(r'[^\s]+\s*([^\s]+).*', result).group(1)
        exit_code = runner.execute_command(
            u"grep '^{group}:' /etc/group", dict(group=group),
            ExecutionOptions(ignore_exit_code=True)
        )
        if exit_code != 0:
            return None
        logger.fdebug(messages.DETECTED_APACHE_GROUP, group=group)
        return group
    except:
        return None


def copy_file_secure(runner, user, source_file_path, target_file_path):
    """Copy file under specified user in a secure way, set permissions so only user could read the file

    Use instead of "cp under root to target path" + "chmod" + "chown" scheme to avoid potential security problems
    with hardlinks.
    Use instead of "cp under root to a safe place" + "chmod" + "chown" + "mv under root to target path" scheme
    to avoid problems when source and target files are located on different filesystems.

    :type runner: parallels.core.runners.base.BaseRunner
    :type user: str
    :type source_file_path: str
    :type target_file_path: str
    :rtype: None
    """
    runner.execute_command(
        'cat {source_file_path} | su {username} --shell /bin/sh -c {command}',
        dict(
            source_file_path=source_file_path,
            username=user,
            # "umask 0377" gives "-r--------" permissions to a file - so only user could only read the file
            command=format_command('umask 0377 && cat - > {target_file}', target_file=target_file_path)
        )
    )


def move_file_secure(runner, user, source_file_path, target_file_path):
    """Move file under specified user in a secure way, set permissions so only user could read the file

    Use instead of "chmod" + "chown" + "mv under root to target path" scheme
    to avoid problems when source and target files are located on different filesystems.
    Use instead of "cp under root to target path" + "chmod" + "chown" + "remove source file" scheme
    to avoid potential security problems with hardlinks.

    :type runner: parallels.core.runners.base.BaseRunner
    :type user: str
    :type source_file_path: str
    :type target_file_path: str
    :rtype: None
    """
    copy_file_secure(runner, user, source_file_path, target_file_path)
    runner.remove_file(source_file_path)
