import os
import ntpath
from collections import namedtuple

from parallels.core.logging import get_logger
from parallels.core import messages
from parallels.core import MigrationError
from parallels.core.connections.plesk_server import PleskServer
from parallels.core.runners.base import BaseRunner
from parallels.core.runners.entities import ExecutionOptions
from parallels.core.utils.download_utils import download
from parallels.core.utils import windows_utils
from parallels.core.utils.common.threading_utils import LocksByKey
from parallels.core.utils.message_utils import multi_line_message
from parallels.core.utils.steps_profiler import sleep
from parallels.core.utils import windows_thirdparty
from parallels.core.utils.windows_utils import get_binary_full_path

logger = get_logger(__name__)

RsyncConfig = namedtuple('RsyncConfig', (
    'vhosts_dir', 'migrator_dir', 'user', 'password'
))

install_locks = LocksByKey()


class RsyncInstaller(object):
    """Install rsync on remote Windows node"""

    def __init__(self, server):
        self.server = server

    def install(self):
        base_path = self.server.get_session_file_path(u'rsync\\bin')

        with self.server.runner() as runner, install_locks.lock(self.server):
            assert isinstance(runner, BaseRunner)
            storage_path = windows_thirdparty.get_rsync_installer_bin()
            if os.path.exists(storage_path) is False:
                storage_url = 'http://autoinstall.plesk.com/panel-migrator/thirdparties/panel-migrator-rsync-installer-2.exe'
                logger.info(messages.DOWNLOAD_RSYNC.format(url=storage_url))
                download(storage_url, storage_path)
            installer_path = self.server.get_session_file_path('panel-migrator-rsync-installer-2.exe')
            logger.info(messages.UPLOAD_RSYNC.format(path=installer_path, server=self.server.description()))
            runner.upload_file(windows_thirdparty.get_rsync_installer_bin(), installer_path)
            logger.info(messages.INSTALL_RSYNC.format(server=self.server.description()))
            runner.execute_command(r'{installer} -o{session_dir} -y', dict(
                installer=installer_path,
                session_dir=self.server.get_session_dir_path()
            ))

        return base_path


class RsyncInstance(object):
    def __init__(self, base_path):
        self.base_path = base_path


class RsyncClient(RsyncInstance):
    pass


class RsyncServer(RsyncInstance):
    FIREWALL_RULE = 'Plesk Migrator Rsync Server'

    def __init__(self, base_path, server):
        super(RsyncServer, self).__init__(base_path)
        self._config = None
        self._server = server
        self._executable_path = ntpath.join(base_path, 'panel-migrator-rsync.exe')
        self._config_path = ntpath.join(base_path, 'rsyncd.conf')
        self._secrets_path = ntpath.join(base_path, 'rsyncd.secrets')

    def configure(self, config):
        with self._server.runner() as runner:
            logger.info(messages.CONFIGURE_RSYNC_SERVER_RSYNCDCONF_AND_RSYNCDSECRETS)

            rsync_config_content = multi_line_message("""
                use chroot = false
                strict modes = false
                reverse lookup = false
                hosts allow = *
                log file = rsyncd.log
                secrets file = rsyncd.secrets
                port = 10156

                [vhosts]
                path = {vhosts_dir}
                read only = true
                transfer logging = yes
                auth users = {user}

                [migrator]
                path = {migrator_dir}
                read only = false
                transfer logging = yes
                auth users = {user}
            """)
            placeholders = dict(
                vhosts_dir=windows_utils.convert_path_to_cygwin(
                    config.vhosts_dir
                ) if config.vhosts_dir is not None else '/cygdrive/c',  # XXX
                migrator_dir=windows_utils.convert_path_to_cygwin(config.migrator_dir),
                user=config.user,
            )
            if isinstance(self._server, PleskServer) and self._server.plesk_version >= (17, 0):
                rsync_config_content += '\n\n' + multi_line_message("""
                    [extensions]
                    path = {extensions_dir}
                    read only = true
                    transfer logging = yes
                    auth users = {user}
                """)
                placeholders['extensions_dir'] = windows_utils.convert_path_to_cygwin(
                    self._server.get_extensions_var_dir()
                )

            rsync_config = rsync_config_content.replace('\n', '\r\n').format(**placeholders)

            secrets_file = "{user}:{password}".format(
                user=config.user,
                password=config.password
            )
            runner.upload_file_content(self._config_path, rsync_config)
            runner.upload_file_content(self._secrets_path, secrets_file)

        self._config = config

    def restart(self):
        with self._server.runner() as runner:
            assert isinstance(runner, BaseRunner)
            # Add firewall rule before rsync service start to prevent Windows notifications
            try:
                runner.delete_allowed_program_firewall_rule(RsyncServer.FIREWALL_RULE, self._executable_path)
                runner.add_allowed_program_firewall_rule(RsyncServer.FIREWALL_RULE, self._executable_path)
            except:
                logger.exception()
                logger.warning(messages.FAILED_TO_ADD_ALLOWED_PROGRAM_FIREWALL_RULE.format(
                    rule=RsyncServer.FIREWALL_RULE, program=self._executable_path))

            try:
                logger.info(messages.STOP_RSYNC_IF_IT_IS_RUNNING)

                tasks_list = runner.execute_command(
                    '{tasklist_bin} /FI "IMAGENAME eq panel-migrator-rsync.exe"',
                    dict(tasklist_bin=get_binary_full_path(self._server, 'tasklist')),
                    ExecutionOptions(output_encoding=runner.native_output_encoding)
                ).stdout
                if 'panel-migrator-rsync.exe' in tasks_list:
                    runner.execute_command(
                        r'{taskkill_bin} /im panel-migrator-rsync.exe /f',
                        dict(taskkill_bin=get_binary_full_path(self._server, 'taskkill')),
                        ExecutionOptions(output_encoding=runner.native_output_encoding)
                    )
                runner.execute_command(
                    "{rsync_bin} --daemon --config rsyncd.conf",
                    dict(
                        rsync_bin=self._executable_path,
                    ),
                    ExecutionOptions(working_dir=self.base_path)
                )
            except:
                try:
                    runner.delete_allowed_program_firewall_rule(RsyncServer.FIREWALL_RULE, self._executable_path)
                except:
                    logger.exception()
                    logger.warning(messages.FAILED_TO_DELETE_ALLOWED_PROGRAM_FIREWALL_RULE.format(
                        rule=RsyncServer.FIREWALL_RULE, program=self._executable_path))
                raise

    def stop(self):
        with self._server.runner() as runner:
            assert isinstance(runner, BaseRunner)
            try:
                runner.delete_allowed_program_firewall_rule(RsyncServer.FIREWALL_RULE, self._executable_path)
            except:
                logger.exception()
                logger.warning(messages.FAILED_TO_DELETE_ALLOWED_PROGRAM_FIREWALL_RULE.format(
                    rule=RsyncServer.FIREWALL_RULE, program=self._executable_path))
            # Stop rsync server
            runner.execute_command(
                r'{taskkill_bin} /im panel-migrator-rsync.exe /f',
                dict(taskkill_bin=get_binary_full_path(self._server, 'taskkill')),
                ExecutionOptions(output_encoding=runner.native_output_encoding)
            )

    def set_vhosts_dir(self, vhosts_dir):
        if self._config is None:
            raise Exception(messages.UNABLE_SET_VHOSTS_DIR_RSYNC_SERVER % self.base_path)
        new_config = RsyncConfig(
            vhosts_dir=vhosts_dir,
            migrator_dir=self._config.migrator_dir,
            user=self._config.user,
            password=self._config.password
        )
        self.configure(new_config)

    @property
    def vhosts_dir(self):
        return self._config.vhosts_dir if self._config is not None else None

    @property
    def login(self):
        return self._config.user if self._config is not None else None

    @property
    def password(self):
        return self._config.password if self._config is not None else None


class RsyncControl(object):
    """Simple object to work with provided rsync client and rsync server"""
    def __init__(self, target, target_rsync_bin, source_ip, source_login, source_password, server=None):
        self._target = target
        self._target_rsync_bin = target_rsync_bin
        self._source_ip = source_ip
        self._source_login = source_login
        self._source_password = source_password
        self._server = server
        self._max_attempts = 5
        self._attempts_before_restart = 2
        self._interval_between_attempts = 10

    @staticmethod
    def get_rsync_path_to_source_session_dir_file(filename):
        """Get path that could be used by rsync commands for a file located in session directory on the source server

        :param str filename: path to a file on a source server relative to session directory
        :rtype: str
        """
        return "migrator\%s" % filename

    def sync(self, source_path, target_path, exclude=None, rsync_additional_args=None, is_directory=False):
        env = {}
        if self._source_password is not None:
            env['RSYNC_PASSWORD'] = self._source_password.encode('utf-8')

        if self._source_login is not None:
            login_clause = '%s@' % (self._source_login,)
        else:
            login_clause = ''

        source_rsync_path = u"rsync://{login_clause}{source_ip}{port_clause}/{source_path}".format(
            login_clause=login_clause,
            source_ip=self._source_ip,
            port_clause=self._get_port_clause(),
            source_path=source_path + ('/' if is_directory else '')
        )

        additional_args = ""
        if rsync_additional_args is not None and len(rsync_additional_args) > 0:
            additional_args += u" ".join([arg for arg in rsync_additional_args]) + " "

        cmd = (
            u'{rsync_bin} --no-perms -t -r --timeout=30 %s{source_rsync_path} {target_path}' % additional_args
        )
        args = dict(
            rsync_bin=self._target_rsync_bin,
            source_rsync_path=source_rsync_path,
            target_path=target_path + ('/' if is_directory else '')
        )

        if exclude is not None and len(exclude) > 0:
            for i, ex in enumerate(exclude):
                cmd += u" --exclude {exclude_%s}" % i
                args['exclude_%s' % i] = ex

        return self._run_command(cmd, args, env)

    def upload(self, source_path, target_path):
        env = {}
        if self._source_password is not None:
            env['RSYNC_PASSWORD'] = self._source_password.encode('utf-8')

        if self._source_login is not None:
            login_clause = '%s@' % (self._source_login,)
        else:
            login_clause = ''

        rsync_target_path = u"rsync://{login_clause}{source_ip}{port}/{target_path}".format(
            login_clause=login_clause,
            source_ip=self._source_ip,
            port=self._get_port_clause(),
            target_path=target_path,
        )

        cmd = u'{rsync_bin} --no-perms -t -r --timeout=30 {source_path} {rsync_target_path}'

        args = dict(
            rsync_bin=self._target_rsync_bin,
            source_path=source_path,
            rsync_target_path=rsync_target_path,
        )

        self._run_command(cmd, args, env)

    def list_files(self, source_path):
        """
        :type source_path: str | unicode 
        :rtype: None 
        """
        env = {}
        if self._source_password is not None:
            env['RSYNC_PASSWORD'] = self._source_password.encode('utf-8')

        if self._source_login is not None:
            login_clause = '%s@' % (self._source_login,)
        else:
            login_clause = ''

        rsync_data_path = u"rsync://{login_clause}{source_ip}{port}/{source_path}".format(
            login_clause=login_clause,
            source_ip=self._source_ip,
            port=self._get_port_clause(),
            source_path=source_path,
        )

        with self._target.runner() as runner:
            assert isinstance(runner, BaseRunner)
            execution_results = runner.execute_command(
                u'{rsync_binary_path} -r --timeout=30 --list-only {rsync_data_path)',
                dict(
                    rsync_binary_path=self._target_rsync_bin,
                    rsync_data_path=rsync_data_path
                ),
                ExecutionOptions(
                    ignore_exit_code=True,
                    environment_variables=env
                )
            )

        if execution_results.exit_code != 0:
            raise RsyncClientNonZeroExitCode(
                execution_results.exit_code, execution_results.stdout, execution_results.stderr
            )

    def _run_command(self, cmd, args, env):
        for attempt in range(0, self._max_attempts):
            with self._target.runner() as runner:
                assert isinstance(runner, BaseRunner)
                execution_results = runner.execute_command(
                    cmd, args, ExecutionOptions(ignore_exit_code=True, environment_variables=env)
                )

            if execution_results.exit_code == 0:
                if attempt > 0:
                    logger.info(
                        messages.RSYNC_INTERACTION_OK_AFTER_ATTEMPTS.format(
                            source_ip=self._source_ip, target_ip=self._target.ip(), attempts=attempt + 1
                        )
                    )
                return execution_results.stdout
            else:
                if attempt >= self._max_attempts - 1:
                    raise RsyncClientNonZeroExitCode(
                        execution_results.exit_code, execution_results.stdout, execution_results.stderr
                    )
                else:
                    is_restart = False
                    message = messages.RSYNC_INTERACTION_FAILED
                    if self._server is not None and attempt >= self._attempts_before_restart - 1:
                        message += messages.TRY_RESTART_SERVER_AND_RETRY
                        is_restart = True
                    else:
                        message += ', ' + messages.RSYNC_RETRY_IN
                    logger.error(
                        message.format(
                            source_ip=self._source_ip,
                            target_ip=self._target.ip(),
                            interval=self._interval_between_attempts
                        )
                    )
                    if is_restart:
                        self._server.restart()
                    else:
                        sleep(self._interval_between_attempts, messages.SLEEP_RETRY_RSYNC)

    def _get_port_clause(self):
        if self._server is not None:
            # Server was configured by the migration tool, use non-default port
            return ':10156'
        else:
            # Server was configured by customer, and it should use default 873 port
            return ''


class RsyncClientNonZeroExitCode(MigrationError):
    def __init__(self, exit_code, stdout, stderr):
        self.exit_code = exit_code
        self.stdout = stdout
        self.stderr = stderr
