import ntpath
import posixpath
import random
import re
import string

from parallels.core import messages, MigrationNoContextError
from parallels.core.registry import Registry
from parallels.core.runners.base import BaseRunner
from parallels.core.runners.entities import ExecutionOptions
from parallels.core.runners.unix.local import LocalUnixRunner
from parallels.core.utils.migrator_utils import get_version


class SessionDir(object):

    def __init__(self, runner_cm, session_dir):
        """
        Constructor of migration session storage directory
        :param runner_cm: contextmanager that could create runner object
        :param str session_dir: path to the session directory
        """
        self._runner_cm = runner_cm
        self._base_session_dir = session_dir
        self._session_dir = None

    def get_session_dir_path(self):
        """Return path of a session directory

        :rtype: str
        """
        self._lazy_instantiate()
        return self._session_dir

    def get_file_path(self, *args):
        """Return full path to a file identified by given args

        :rtype: str
        """
        self._lazy_instantiate()
        if len(args) > 1:
            # create parent directory if not exists
            dir_path = self._get_file_path(*args[:-1])
            with self._runner_cm() as runner:
                if not runner.file_exists(dir_path):
                    runner.mkdir(dir_path)
        return self._get_file_path(*args)

    def is_file_exists(self, file_path):
        with self._runner_cm() as runner:
            return runner.file_exists(file_path)

    def read_file(self, file_path):
        with self._runner_cm() as runner:
            return runner.get_file_contents(file_path)

    def write_file(self, file_path, file_content):
        with self._runner_cm() as runner:
            runner.upload_file_content(file_path, file_content)

    def _lazy_instantiate(self):
        if self._session_dir is not None:
            return

        self._session_dir = self._create()

    def _create(self):
        return self._create_versioned_session_dir_in_secure_path(self._base_session_dir)

    def _create_versioned_session_dir_in_secure_path(self, base_session_dir):
        """Create versioned session directory which is located in secure path.

        Secure path means that base session directory is located in path that can be accessed only
        by users to which we want to provide access to the session directory.
        For example,
            /usr/local/psa/var/modules/panel-migrator/sessions is secure,
        while:
            /usr/local/psa/var/modules/panel-migrator/ is not (readable by others)
            /tmp is not (readable/writable by others)

        Versioned means that special "version" file will be created to control migrator upgrades and clean up
        cache files in such case.

        :rtype: str | unicode
        """
        with self._runner_cm() as runner:
            if not runner.file_exists(base_session_dir):
                # session directory does not exist, so create it
                runner.mkdir(base_session_dir)
                if not isinstance(runner, LocalUnixRunner):
                    self._set_permissions(runner, base_session_dir)
                self._create_version_file(base_session_dir, get_version())
            else:
                # compare existing session directory version with Panel Migrator version
                version_file_name = self._join_paths(base_session_dir, 'version')
                is_backup = False
                if not runner.file_exists(version_file_name):
                    is_backup = True
                else:
                    version = runner.get_file_contents(version_file_name).strip()
                    if version != get_version():
                        is_backup = True
                if is_backup:
                    # session directory version does not match with Panel Migrator version,
                    # so backup all sensitive data: move it out from their regular location into
                    # separate directory
                    self._backup(base_session_dir, get_version())
                    self._create_version_file(base_session_dir, get_version())

            return base_session_dir

    def _get_file_path(self, *args):
        return self._join_paths(self._session_dir, *args)

    def _backup(self, versioned_dir, version):
        do_not_backup_files = [
            # do not backup generated passwords file to prevent repeat of passwords generation
            r'^generated-passwords\.yaml$',
            # do not backup name mapping to keep old database/database user renames
            r'^name-mapping\.yaml$',
            # keep old DNS timings, to have ability to restore them
            r'^dns_timings\.yaml$',
            # keep old Apache restart interval, so it could be restored correctly after upgrade
            r'^apache_restart_interval_.*\.yaml$',
            # keep default hosting description file names, which are usually written by migrator user and so
            # should not be touched; also they should be backward-compatible.
            r'^hosting.description.*\.yaml$',
            r'^hd.*\.yaml$'
        ]

        with self._runner_cm() as runner:
            assert isinstance(runner, BaseRunner)
            files_to_move = []
            for file_name in runner.get_files_list(versioned_dir):
                if any(re.match(f, file_name) is not None for f in do_not_backup_files):
                    continue
                if file_name.endswith('.yaml'):
                    # backup all rest yaml files, because format of storing data may change
                    # and become invalid after upgrade
                    files_to_move.append(file_name)
                if file_name in {'migration-node.crt', 'migration-node.key', 'source-node.crt', 'source-node.key'}:
                    # backup certificate-key pair to perform upgrade of agent on source Windows nodes
                    files_to_move.append(file_name)
            if len(files_to_move) > 0:
                backup_path = self._join_paths(versioned_dir, '_backup_%s' % version)
                runner.mkdir(backup_path)
                for file_name in files_to_move:
                    runner.move(
                        self._join_paths(versioned_dir, file_name),
                        self._join_paths(backup_path, file_name)
                    )

    def _create_version_file(self, versioned_dir, version):
        with self._runner_cm() as runner:
            version_file_path = self._join_paths(versioned_dir, 'version')
            if runner.file_exists(version_file_path):
                runner.remove_file(version_file_path)
            runner.upload_file_content(version_file_path, version)

    def _join_paths(self, *paths):
        raise NotImplementedError()

    def _set_permissions(self, runner, directory):
        raise NotImplementedError()


class WindowsSessionDir(SessionDir):
    """
    Migration session storage directory on Windows
    """
    def _join_paths(self, *paths):
        return ntpath.join(*paths)

    def _set_permissions(self, runner, directory):
        """
        :type runner: parallels.core.runners.base.BaseRunner 
        """
        context = Registry.get_instance().get_context()
        if context is not None:
            config = context.config
            is_skip = (
                'skip-set-session-directory-permissions' in config.options('GLOBAL') and
                config.getboolean('GLOBAL', 'skip-set-session-directory-permissions')
            )
        else:
            is_skip = False

        if is_skip:
            return
        # check if setting inheritance is allowed by icacls built-in tool
        # by checking it's help
        if (
            '/inheritance' in runner.execute_command(
                'icacls /?',
                execution_options=ExecutionOptions(
                    output_encoding=runner.native_output_encoding,
                    log_output=False
                )
            ).stdout
        ):
            icacl_commands = [
                '/inheritance:r',
                # "S-1-5-32-544" is a security identifier of build-in Administrators group.
                # Check https://support.microsoft.com/en-us/kb/243330 for the list of well-known security identifiers.
                # We use SID instead of name, because the group may be named in other way, or renamed.
                # For example, in German Windows it is called "Administratoren".
                '/grant *S-1-5-32-544:(OI)(CI)F',
                '/grant SYSTEM:(OI)(CI)F'
            ]
            for icacl_command in icacl_commands:
                runner.execute_command(
                    "icacls {session_dir} %s" % (icacl_command,),
                    dict(session_dir=directory),
                    ExecutionOptions(output_encoding=runner.native_output_encoding)
                )
        else:
            # If there is no 'inheritance' option for icacls, there is no
            # simple way to set permissions allowing to temporary migration
            # files only by Administrator. That is possible on Windows
            # 2003 Permissions will be inherited, and we consider that it
            # is a migration tool's user responsibility to set permissions
            # for a directory for temporary files
            pass


class UnixSessionDir(SessionDir):
    """
    Migration session storage directory on Unix
    """
    def __init__(self, runner_cm, session_dir, secure_subdir=True):
        """
        Constructor of migration session storage directory
        :param runner_cm: contextmanager that could create runner object
        :param str session_dir: path to the session directory
        :param bool secure_subdir: whether to store files in directory specified by "session_dir" parameter,
            or create separate secure directory inside of directory specified by "session_dir" parameter;
            set False only if you are sure that directory specified by "session_dir" is 100% secure and no
            one could create/read files/directoried in it except for migrator users
        """
        super(UnixSessionDir, self).__init__(runner_cm, session_dir)
        self._secure_subdir = secure_subdir

    def _create(self):
        if not self._secure_subdir:
            return self._create_versioned_session_dir_in_secure_path(self._base_session_dir)
        else:
            return self._create_secure_subdir(self._base_session_dir)

    def _create_secure_subdir(self, base_session_dir):
        with self._runner_cm() as runner:
            assert isinstance(runner, BaseRunner)
            if not runner.file_exists(base_session_dir):
                # session base directory does not exist, so create it
                runner.mkdir(base_session_dir)  # equivalent to "mkdir -p", we don't care of permissions here

            random_suffix_length = 32
            attempts = 10
            prefix = 'plesk_migrator'

            session_dir = None

            # try to find random unused name for a directory inside of base session directory
            for attempt in range(attempts):
                suffix = u''.join(
                    random.choice(string.digits + string.ascii_lowercase) for _ in range(random_suffix_length)
                )
                generated_dirname = u'%s-%s' % (prefix, suffix)
                generated_dir = self._join_paths(base_session_dir, generated_dirname)
                if not runner.file_exists(generated_dir):
                    session_dir = self._join_paths(base_session_dir, generated_dirname)
                    break

            if session_dir is None:
                # no names are available (you're very unlucky or there is hacker's attack)
                raise MigrationNoContextError(messages.FAILED_TO_SELECT_SESSION_DIRECTORY_NAME)

            # try to create directory; if it already exists (was created by hacker: we just checked that it does not
            # exist), then this command fails, and migration fails completely.
            runner.execute_command('mkdir -m 700 {dir}', dict(dir=session_dir))

            return session_dir

    def _join_paths(self, *paths):
        return posixpath.join(*paths)

    def _set_permissions(self, runner, directory):
        """
        :type runner: parallels.core.runners.base.BaseRunner
        """
        runner.execute_command('chmod u=rwx,go= {session_dir}', dict(session_dir=directory))
