import sys
from subprocess import list2cmdline
import ntpath
import re

import os

from parallels.core.logging import get_logger
from parallels.core import messages, MigrationError
from parallels.core.runners.base import BaseRunner
from parallels.core.runners.entities import ExecutionOptions
from parallels.core.utils.common import cached, is_run_on_windows

logger = get_logger(__name__)


def format_command(template, *args, **kw):
    qargs = map(_quote_arg, args)
    qkw = dict((k, _quote_arg(v)) for k, v in kw.items())
    if sys.version_info.major >= 3:
        str_type = str
    else:
        str_type = unicode
    return str_type(template).format(*qargs, **qkw)


def format_command_dict(cmd, args):
    """Format Windows command, with arguments dictionary

    :type cmd: str | unicode
    :type args: dict | None
    :rtype: str | unicode
    """
    if args is None:
        args = {}

    # noinspection PyArgumentList
    return format_command(cmd, **args)


def format_command_list(cmd, args):
    return " ".join([
        _quote_arg(arg) for arg in [cmd] + ([] if args is None else args)
    ])


def format_args_list(args):
    return " ".join([
        _quote_arg(arg) for arg in ([] if args is None else args)
    ])


def convert_path_to_cygwin(path):
    drive, rel_path = ntpath.splitdrive(path)
    if drive != '':
        drive = drive[0].lower()
        rel_path = rel_path[1:].replace("\\", "/")
        return u"/cygdrive/%s/%s" % (drive, rel_path)
    else:
        return path.replace("\\", "/")


def to_cmd(pathname):
    return pathname.replace('/', '\\')


def path_join(base_path, *paths):
    """Join the paths using backslash as a separator, and return the result.
    Take care of the excess separators between the joined path chunks.

    This function behaves similar to ntpath.join
    path_join('\\\\sambashare\\dir1\\', 'file1') returns the same '\\\\sambashare\\dir1\\file1'
    as ntpath.join('\\\\sambashare\\dir1\\', 'file1')
    but does not allow a subsequent path to override all the paths before:
    path_join('\\dir1', '\\file1') returns '\\dir1\\file1',
    whereas ntpath.join('\\dir1', '\\file1') returns '\\file1'.
    It also takes more care of the excess separators than ntpath.join does:
    path_join('\\dir1\\\\dir2\\\\', '\\file1') returns '\\dir1\\\\dir2\\file1',
    whereas ntpath.join('\\dir1\\\\dir2\\\\', '\\file1') returns '\\dir1\\\\dir2\\\\file1'.

    :rtype: str | unicode
    """
    if len(paths) == 0:
        return base_path

    sep_to_strip = r'\/'
    sep_to_add = '\\'

    joined_path = base_path.rstrip(sep_to_strip)

    for path in paths[:-1]:
        joined_path += sep_to_add + path.strip(sep_to_strip)
    joined_path += sep_to_add + paths[-1].lstrip(sep_to_strip)

    return joined_path


def fix_path(path):
    """Fix incorrect and double slashes in Windows path"""
    return path.replace('//', '/').replace('/', '\\')


def convert_path(subscription, source_path, source_vhost_path):
    """Convert path from source to target
    Paths are normalized before comparison and replacement.
    Return normalized result path.
    """
    def normalize_path(p):
        return p.replace('/', '\\').rstrip('\\')

    source_path = normalize_path(source_path)
    source_vhost_path = normalize_path(source_vhost_path)
    new_vhost_path = normalize_path(path_join(subscription.web_target_server.vhosts_dir, subscription.name_idn))

    if source_path.lower().startswith(source_vhost_path.lower() + '\\'):
        new_vdir_path = path_join(new_vhost_path, source_path[len(source_vhost_path) + 1:])
    elif source_path.lower() == source_vhost_path.lower():
        new_vdir_path = new_vhost_path
    else:
        new_vdir_path = source_path

    # if we don't strip double backslash, error documents of File type will not work after transfer.
    return new_vdir_path.replace('\\\\', '\\')


def get_from_registry(runner, keys, value):
    """Get value from Windows registry

    :type runner: parallels.core.runners.base.BaseRunner
    :type keys: list[str]
    :type value: str
    :rtype: str
    """
    for key in keys:
        execution_results = runner.execute_command(
            'reg query {key} /v {value}', dict(key=key, value=value),
            ExecutionOptions(ignore_exit_code=True)
        )
        if execution_results.exit_code == 0:
            match = re.search(
                '^\s*%s\s+REG_(EXPAND_)?SZ\s+(.*?)\s*$' % re.escape(value), execution_results.stdout, re.MULTILINE
            )
            if match is not None:
                return match.group(2).strip()
    return None


def find_in_registry(runner, keys, pattern):
    """
    Return true if key with given pattern exist below one of given keys in registry, false otherwise
    :type runner: parallels.core.runners.base.BaseRunner
    :type keys: list[str]
    :type pattern: str
    :rtype: bool
    """
    for key in keys:
        command_res = runner.execute_command(
            'reg query {key}', dict(key=key), ExecutionOptions(ignore_exit_code=True)
        )
        if command_res.exit_code == 0:
            match = re.search('^%s%s$' % (re.escape(key + '\\'), pattern), command_res.stdout, re.MULTILINE)
            if match is not None:
                return True
    return False


def normalize_path(path):
    """Normalize Windows path for comparison - use single slash style, remove leading slash, convert to lowercase

    :type path: str | unicode
    :rtype: str | unicode
    """
    return path.replace('/', '\\').rstrip('\\').lower()


def is_built_in_administrator(username):
    sid = _get_username_sid(username)
    return _is_built_in_administrator_sid(sid)


def is_run_by_built_in_administrator():
    sid = _get_current_process_sid()
    return _is_built_in_administrator_sid(sid)


def is_run_with_elevated_privileges():
    sid = _get_current_process_sid()
    return _is_built_in_administrator_sid(sid) or _is_system_sid(sid)


def _get_username_sid(username):
    import win32net
    import win32security
    import pywintypes
    try:
        info = win32net.NetUserGetInfo(None, username, 4)
    except pywintypes.error as error:
        if error.winerror == 2221:
            # process error code NERR_UserNotFound, consider what not existing user is not built-in administrator
            # see details at https://msdn.microsoft.com/en-us/library/windows/desktop/aa370654(v=vs.85).aspx
            return False
        raise error
    sid = info['user_sid']
    logger.debug(messages.UTIL_WINDOWS_GET_USERNAME_SID.format(
        username=username,
        sid=win32security.ConvertSidToStringSid(sid)
    ))
    return sid


def _get_current_process_sid():
    import win32process
    import win32security

    handle = win32process.GetCurrentProcess()
    token = win32security.OpenProcessToken(handle, win32security.TOKEN_QUERY)
    sid, _ = win32security.GetTokenInformation(token, 1)

    logger.debug(messages.UTIL_WINDOWS_GET_CURRENT_PROCESS_SID.format(
        sid=win32security.ConvertSidToStringSid(sid)
    ))

    return sid


def _is_built_in_administrator_sid(sid):
    # check if given SID is well known for built-in system administrator
    # according https://msdn.microsoft.com/en-us/library/cc980032.aspx
    if (0, 0, 0, 0, 0, 5) != sid.GetSidIdentifierAuthority():
        return False
    sub_authority_count = sid.GetSubAuthorityCount()
    if sub_authority_count < 1:
        return False
    if sid.GetSubAuthority(0) != 21:
        return False
    if sid.GetSubAuthority(sub_authority_count - 1) != 500:
        return False
    return True


def _is_system_sid(sid):
    import win32security
    # check if given SID is well known for account that is used by the operating system
    # according https://msdn.microsoft.com/en-us/library/cc980032.aspx
    return win32security.ConvertSidToStringSid(sid) == 'S-1-5-18'


def get_security_policy(server):
    """
    :type server: parallels.core.connections.target_servers.TargetServer
    """
    security_policy_sdl = server.get_session_file_path('security_policy.sdl')
    with server.runner() as runner:
        assert isinstance(runner, BaseRunner)
        # secedit requires some file for export command, so we create and empty one
        runner.upload_file_content(security_policy_sdl, "")
        runner.execute_command(
            'secedit /export /db {sdb_file} /areas SECURITYPOLICY /cfg {sdl_file}',
            dict(
                sdb_file=ntpath.join(get_system_root(server), 'Security\Database\secedit.sdb'),
                sdl_file=security_policy_sdl
            ),
            ExecutionOptions(output_encoding=runner.native_output_encoding)
        )
        security_policy_content = runner.get_file_contents(security_policy_sdl)
        runner.remove_file(security_policy_sdl)
        # Example content of security_policy_content.sdl file:
        # [Version]
        # signature="$CHICAGO$"
        # Revision=1
        # [Profile Description]
        # Description=Default Security Settings. (Windows Server)
        # [System Access]
        # PasswordComplexity = 1

        regex = re.compile('^(\w+)\s*=\s*(.+)$', re.MULTILINE)
        matches = regex.findall(security_policy_content)
        signature = '"$CHICAGO$"'  # default value for signature
        password_policy_value = True  # default value for security policy
        for matched_item in matches:
            if matched_item[0] == 'signature':
                signature = matched_item[1].replace('\r', '')
            elif matched_item[0] == 'PasswordComplexity':
                password_policy_value = False if matched_item[1].replace('\r', '') == '0' else True

        return signature, password_policy_value


def set_security_policy(server, signature, required_policy):
    """
    :type server: parallels.core.connections.target_servers.TargetServer
    :type signature: str
    :type required_policy: bool
    """
    security_policy_sdl = server.get_session_file_path('security_policy.sdl')
    with server.runner() as runner:
        assert isinstance(runner, BaseRunner)
        logger.debug(
            messages.DEBUG_ENABLE_SECURITY_POLICY if required_policy else messages.DEBUG_DISABLE_SECURITY_POLICY,
            server.description()
        )
        policy_template = _get_complexity_gpo_template(signature, required_policy)
        runner.upload_file_content(security_policy_sdl, policy_template)
        runner.execute_command(
            'secedit /validate {sdl_file}', dict(sdl_file=security_policy_sdl),
            execution_options=ExecutionOptions(output_encoding=runner.native_output_encoding)
        )
        runner.execute_command(
            'secedit /configure /db {sdb_file} /cfg {sdl_file}', dict(
                sdb_file=ntpath.join(get_system_root(server), 'Security/Database/secedit.sdb'),
                sdl_file=security_policy_sdl
            ),
            ExecutionOptions(output_encoding=runner.native_output_encoding)
        )
        runner.remove_file(security_policy_sdl)


@cached
def get_system_root(server):
    with server.runner() as runner:
        return runner.get_env('SYSTEMROOT')


@cached
def get_path_env(server):
    with server.runner() as runner:
        return runner.get_env('PATH').split(';')


def get_binary_full_path(server, binary_name):
    with server.runner() as runner:
        for path in get_path_env(server):
            full_path = ntpath.join(path, '%s.exe' % binary_name)
            if runner.file_exists(full_path):
                return full_path

    raise MigrationError(messages.UNABLE_TO_FIND_BINARY.format(binary_name=binary_name, server=server.description()))


def add_windows_long_path_prefix(path):
    """Add \\\\?\\ prefix to path to workaround problem with long paths on Windows

    :type path: str | unicode
    :rtype: str | unicode
    """
    if sys.version_info.major >= 3:
        str_type = str
    else:
        str_type = unicode
    if is_run_on_windows():
        if isinstance(path, str_type):
            if not path.startswith(u"\\\\?\\"):
                return u"\\\\?\\" + os.path.abspath(path)
            else:
                return path
        else:
            if not path.startswith("\\\\?\\"):
                return "\\\\?\\" + os.path.abspath(path)
            else:
                return path
    else:
        return path


def _get_complexity_gpo_template(signature, required_policy):
    required_policy = '1' if required_policy else '0'
    lines = ['[Version]', 'signature=%s', '[System Access]', 'PasswordComplexity = %s']
    template = '\r\n'.join(lines)
    return template % (signature, required_policy)


def _quote_arg(arg):
    if sys.version_info.major >= 3:
        str_type = str
    else:
        str_type = unicode
    return list2cmdline([str_type(arg)])


def convert_windows_encoding_to_python(encoding):
    """
    :type encoding: str | unicode
    :rtype: str | unicode
    """
    code_page_map = {
        '65001': 'utf-8'
    }
    if encoding in code_page_map:
        return code_page_map[encoding]

    # this is correct only for limited number of encodings
    # if function works bad, add new encoding mapping to code_page_map dict
    return u"cp%s" % encoding
