import ntpath
import os
import posixpath

from parallels.core import MigrationError, get_logger
from parallels.core.registry import Registry
from parallels.core.runners.base import BaseRunner
from parallels.core.utils import get_thirdparties_base_path
from parallels.core.utils.common import cached, mkdir_p, safe_format
from parallels.core.utils.common.threading_utils import synchronized_by_args, synchronized
from parallels.core.utils.common_constants import AI_URL
from parallels.core.utils.download_utils import download_zip, download
from parallels.plesk import messages


logger = get_logger(__name__)


class ThirdpartyDeployer(object):
    """Deploy third-party package to source and target servers"""

    def __init__(self):
        self._servers = dict()

    @staticmethod
    @synchronized
    @cached
    def get_instance():
        return ThirdpartyDeployer()

    @synchronized_by_args
    def deploy(self, package, server):
        """Deploy third-party package to specified server.

        Returns path to entry point (executable) of that package.

        :type package: parallels.core.utils.thirdparties.package.ThirdpartyPackage
        :type server: parallels.core.connections.server.Server
        :rtype: str | unicode
        """
        if (package, server) not in self._servers:
            store_path = self.deploy_local(package)
            if package.is_self_extractable:
                self._deploy_self_extractable(store_path, server.get_session_dir_path(), server, package)
                package_relative_path = (ntpath if server.is_windows() else posixpath).join(
                    package.package_name + '_' + package.package_version, package.executable
                )
                self._servers[(package, server)] = server.get_session_file_path(package_relative_path)
            else:
                self._transfer_local_dir(store_path, server.get_session_dir_path(), server)
                self._servers[(package, server)] = server.get_session_file_path(package.executable)

            if not server.is_windows():
                with server.runner() as runner:
                    assert isinstance(runner, BaseRunner)
                    runner.execute_command('chmod +x {executable}', dict(executable=self._servers[(package, server)]))

        return self._servers[(package, server)]

    def get_executable(self, package, server):
        """Returns path to entry point (executable) of package.

        :type package: parallels.core.utils.thirdparties.package.ThirdpartyPackage
        :type server: parallels.core.connections.server.Server
        :rtype: str | unicode | None
        """
        if (package, server) in self._servers:
            return self._servers[(package, server)]
        else:
            raise MigrationError(safe_format(
                messages.PACKAGE_NOT_DEPLOYED, package=package.package_name, server=server.description()
            ))

    @synchronized_by_args
    @cached
    def deploy_local(self, package):
        """Download package from autoinstall.plesk.com server, put to local thirdparties directory

        :type package: parallels.core.utils.thirdparties.package.ThirdpartyPackage
        :rtype: str | unicode
        """
        server = Registry.get_instance().get_context().migrator_server
        thirdparties_base_path = get_thirdparties_base_path()
        package_file = '{package}_{version}.{format}'.format(
            ai_url=AI_URL, package=package.package_name, version=package.package_version,
            format='exe' if package.is_self_extractable else 'zip'
        )
        if not os.path.isdir(thirdparties_base_path):
            mkdir_p(thirdparties_base_path)
        if not server.is_windows() and package.available_for_all:
            os.chmod(thirdparties_base_path, 0o755)
        if package.is_self_extractable:
            store_path = os.path.join(thirdparties_base_path, package_file)
            if not os.path.exists(store_path):
                store_url = '{ai_url}/panel-migrator/thirdparties/{package_file}'.format(
                    ai_url=AI_URL, package_file=package_file
                )
                download(store_url, store_path)
        else:
            store_path = os.path.join(
                thirdparties_base_path,
                '{package}_{version}'.format(package=package.package_name, version=package.package_version)
            )
            if not os.path.exists(store_path):
                store_url = '{ai_url}/panel-migrator/thirdparties/{package_file}'.format(
                    ai_url=AI_URL, package_file=package_file
                )
                download_zip(store_url, store_path)

                if not server.is_windows() and package.available_for_all:
                    self.set_permissions(store_path, dir_mode=0o755, file_mode=0o644)

            if not server.is_windows():
                with server.runner() as runner:
                    assert isinstance(runner, BaseRunner)
                    runner.execute_command(
                        'chmod 0755 {executable}', dict(executable=os.path.join(store_path, package.executable))
                    )

        return store_path

    @staticmethod
    def set_permissions(path, dir_mode, file_mode):
        """Recursive set permissions for path

        :type path: str | unicode
        :type dir_mode: int
        :type file_mode: int
        :rtype: None
        """
        for root, dirs, files in os.walk(path):
            os.chmod(path, dir_mode)
            for d in dirs:
                os.chmod(os.path.join(root, d), dir_mode)
            for f in files:
                os.chmod(os.path.join(root, f), file_mode)

    def _transfer_local_dir(self, local_dir, remote_dir, server):
        """Transfer local directory to remote server

        :type local_dir: str | unicode
        :type remote_dir: str | unicode
        :type server: parallels.core.connections.server.Server
        :rtype: None
        """
        with server.runner() as runner:
            for file_name in os.listdir(local_dir):
                local_path = os.path.join(local_dir, file_name)
                remote_path = (ntpath if server.is_windows() else posixpath).join(
                    remote_dir, file_name
                )
                if os.path.isfile(local_path):
                    runner.upload_file(local_path, remote_path)
                else:
                    runner.mkdir(remote_path)
                    self._transfer_local_dir(local_path, remote_path, server)

    @staticmethod
    def _deploy_self_extractable(local_file, remote_dir, server, package=None):
        """Upload self-extractable to remote server and extract there

        :type local_file: str | unicode
        :type remote_dir: str | unicode
        :type server: parallels.core.connections.server.Server
        :type package: parallels.core.utils.thirdparties.package.ThirdpartyPackage | None
        :rtype: None
        """
        with server.runner() as runner:
            assert isinstance(runner, BaseRunner)
            file_name = os.path.basename(local_file)
            remote_path = (ntpath if server.is_windows() else posixpath).join(remote_dir, file_name)

            flag = safe_format('{path}.installed', path=remote_path)
            if runner.file_exists(flag):
                if package is None:
                    package_name = file_name
                else:
                    package_name = package.package_name + u' ' + package.package_version
                logger.finfo(messages.PACKAGE_ALREADY_DEPLOYED, package=package_name, server=server.description())
                return

            if os.path.isfile(local_file):
                runner.upload_file(local_file, remote_path)
                runner.execute_command(
                    safe_format('{bin} -y -gm2 -o{path}'),
                    dict(bin=remote_path, path=remote_path.rstrip('.exe'))
                )
                runner.upload_file_content(flag, '')
