from parallels.core.logging import get_logger
from parallels.core.registry import Registry
from parallels.core.utils.common import default, generate_random_password, is_empty
from parallels.core.utils.entity import Entity

logger = get_logger(__name__)


class PleskObjectCreator(object):
    """Base class for algorithm to create object with login"""
    def __init__(self, object_type, arguments, static_arguments=None):
        """Class constructor

        Arguments is a list of dynamically expanded arguments that depend on passed object.
        Static arguments is a list of options (list of strings) that do not depend on passed object.

        :type object_type: str | unicode
        :type arguments: list[parallels.plesk.hosting_repository.utils.object_creator.PleskCommandArgument]
        :type static_arguments: list[str | unicode] | None
        """
        self._plesk_cli_runner = Registry.get_instance().get_context().conn.target.plesk_cli_runner
        self._object_type = object_type
        self._arguments = arguments
        self._static_arguments = default(static_arguments, [])

    def create(self, obj):
        """Create object on the target Plesk server

        :type obj: parallels.core.target_data_model.LoginObject
        :rtype: list[parallels.plesk.hosting_repository.utils.object_creator.PleskCommandArgument]
        """
        raise NotImplementedError()


class PleskObjectCreatorAtOnce(PleskObjectCreator):
    """Creation algorithm: create object with all fields at once"""
    def create(self, obj):
        """Create object on the target Plesk server

        :type obj: parallels.core.target_data_model.LoginObject
        :rtype: list[parallels.plesk.hosting_repository.utils.object_creator.PleskCommandArgument]
        """
        command_args = ['--create', obj.login] + self._static_arguments
        command_env = {}

        for argument in self._arguments:
            args = argument.get_args(obj)
            if args is not None:
                env = argument.get_env(obj)
                command_args.extend(args)
                if env is not None:
                    command_env.update(env)

        command_args.extend(['-notify', 'false'])

        self._plesk_cli_runner.run(self._object_type, command_args, env=command_env)

        return []


class PleskObjectCreatorPartial(PleskObjectCreator):
    """Creation algorithm: create object with all fields empty. Then update fields one-by-one."""
    def create(self, obj):
        """Create object on the target Plesk server

        :type obj: parallels.core.target_data_model.LoginObject
        :rtype: list[parallels.plesk.hosting_repository.utils.object_creator.PleskCommandArgument]
        """
        command_args = [
            '--create', obj.login, '-name', obj.login, '-notify', 'false', '-passwd', ''
        ] + self._static_arguments
        command_env = {'PSA_PASSWORD': generate_random_password()}
        self._plesk_cli_runner.run(self._object_type, command_args, env=command_env)
        failed_args = []

        for argument in self._arguments:
            command_args = ['--update', obj.login]
            command_env = {}

            args = argument.get_args(obj)
            if args is None:
                continue

            command_args.extend(args)
            env = argument.get_env(obj)
            if env is not None:
                command_env.update(env)

            try:
                self._plesk_cli_runner.run(self._object_type, command_args, env=command_env)
            except Exception:
                logger.exception()
                failed_args.append(argument)

        return failed_args


class PleskCommandArgument(Entity):
    """Convert entity (client, reseller) to concrete command line arguments and environment variable values"""
    def __init__(self, title):
        self._title = title

    @property
    def title(self):
        """Title of that argument"

        :rtype: str | unicode
        """
        return self._title

    def get_value(self, obj):
        """Value that could be displayed to customer. Return None if value should not be reported to customer.

        :type obj: parallels.core.target_data_model.LoginObject
        :rtype: str | unicode | None
        """
        raise NotImplementedError()

    def get_args(self, obj):
        """Command line arguments for Plesk command

        :type obj: parallels.core.target_data_model.LoginObject
        :rtype: list[str | unicode] | None
        """
        raise NotImplementedError()

    def get_env(self, obj):
        """Environment variables for Plesk command

        :type obj: parallels.core.target_data_model.LoginObject
        :rtype: dict[str | unicode, str | unicode] | None
        """
        raise NotImplementedError()


class PleskCommandArgumentSimple(PleskCommandArgument):
    def __init__(self, title, name, get_value_func):
        super(PleskCommandArgumentSimple, self).__init__(title)
        self._get_value_func = get_value_func
        self._name = name

    def get_value(self, obj):
        """Value that could be displayed to customer. Return None if value should not be reported to customer.

        :type obj: parallels.core.target_data_model.LoginObject
        :rtype: str | unicode | None
        """
        return self._get_value_func(obj)

    def get_args(self, obj):
        """Command line arguments for Plesk command

        :type obj: parallels.core.target_data_model.LoginObject
        :rtype: list[str | unicode] | None
        """
        value = self._get_value_func(obj)
        if is_empty(value):
            return None
        else:
            return ['-%s' % self._name, self._get_value_func(obj)]

    def get_env(self, obj):
        """Environment variables for Plesk command

        :type obj: parallels.core.target_data_model.LoginObject
        :rtype: dict[str | unicode, str | unicode] | None
        """
        return None


class PleskPasswordArgument(PleskCommandArgument):
    def __init__(self):
        super(PleskPasswordArgument, self).__init__('password')

    def get_value(self, obj):
        """Value that could be displayed to customer. Return None if value should not be reported to customer.

        :type obj: parallels.core.target_data_model.LoginObject
        :rtype: str | unicode | None
        """
        # We don't want to show password to customer in any logs or reports, so return None
        return None

    def get_args(self, obj):
        """Command line arguments for Plesk command

        :type obj: parallels.core.target_data_model.LoginObject
        :rtype: list[str | unicode] | None
        """
        if is_empty(obj.password):
            return None
        else:
            return ['-passwd', '']

    def get_env(self, obj):
        """Environment variables for Plesk command

        :type obj: parallels.core.target_data_model.LoginObject
        :rtype: dict[str | unicode, str | unicode] | None
        """
        if is_empty(obj.password):
            return None
        elif obj.password_type == 'encrypted':
            return {'PSA_CRYPTED_PASSWORD': obj.password}
        else:  # consider this is plain text password
            return {'PSA_PASSWORD': obj.password}


def format_failed_fields_list(failed_fields, obj):
    """Format list of fields that migrator failed to set for an object

    Returns tuple of two elements.
    The first one is a string to put into additional information field, so
    we don't loose data that we failed to set to proper field.
    The second one is a string to put into report message.

    :type failed_fields: list[parallels.plesk.hosting_repository.utils.object_creator.PleskCommandArgument]
    :type obj: parallels.core.target_data_model.LoginObject
    :rtype: tuple(str | unicode)
    """
    additional_info = []
    error_message = []

    if len(failed_fields) > 0:
        for reseller_command_argument in failed_fields:
            value = reseller_command_argument.get_value(obj)
            if value is None:
                error_message.append("- %s" % reseller_command_argument.title)
                # notice: None is a special value meaning we don't want to show the value
            else:
                additional_info.append("%s: %s" % (reseller_command_argument.title, value))
                error_message.append("- %s ('%s')" % (reseller_command_argument.title, value))

    return "\n".join(additional_info), "\n".join(error_message)
