from parallels.core import MigrationError
from parallels.core.logging import get_logger
from parallels.core.hosting_repository.reseller import ResellerModel, ResellerEntity
from parallels.core.registry import Registry
from parallels.core.utils.common import join_nonempty_strs, cached, safe_format, safe_string_repr
from parallels.plesk import messages
from parallels.plesk.hosting_repository.base import PleskBaseModel
from parallels.plesk.hosting_repository.utils.cli.reseller import ResellerExternalIdCli
from parallels.plesk.hosting_repository.utils.cli.reseller_pref import ResellerPrefUpdateCli, \
    ResellerPrefUpdateLocaleCli
from parallels.plesk.hosting_repository.utils.cli.user import UserUpdateInfoCli
from parallels.plesk.utils.xml_rpc.plesk import operator as plesk_ops
from parallels.core.utils.mysql import escape_args_list
from parallels.plesk.hosting_repository.utils.object_creator import PleskCommandArgumentSimple, \
    PleskPasswordArgument, PleskObjectCreatorAtOnce, PleskObjectCreatorPartial, format_failed_fields_list

logger = get_logger(__name__)


class PleskResellerModel(ResellerModel, PleskBaseModel):
    def get_list(self, filter_username=None):
        """Retrive list of resellers in target Plesk which username listed in given filter

        :type filter_username: list[str] | None
        :rtype: list[parallels.core.hosting_repository.reseller.ResellerEntity]
        """
        plesk_server = Registry.get_instance().get_context().conn.target.plesk_server
        if plesk_server.is_power_user_mode or not plesk_server.can_manage_resellers:
            # There are no resellers in Power User mode
            return []

        if filter_username is not None and len(filter_username) == 0:
            #  filter by username is empty, so no one reseller could be retrieved
            return []

        resellers = []

        query = """
            SELECT id, login, pname, email
            FROM clients
            WHERE type = "reseller"
        """
        query_args = {}
        if filter_username is not None:
            filter_username_placeholders, filter_username_values = escape_args_list(filter_username, 'username')
            query += ' AND login in ({filter_username_placeholders_str})'.format(
                filter_username_placeholders_str=', '.join(filter_username_placeholders)
            )
            query_args.update(filter_username_values)

        rows = self.plesk_server.query_panel_db(query, query_args)

        for row in rows:
            resellers.append(ResellerEntity(
                row['id'],
                row['login'],
                row['pname'],
                '',  # unable to retrieve first name and second name from Plesk separately
                row['email']
            ))

        return resellers

    def create(self, reseller):
        """Create given reseller in target Plesk; return id of created reseller and list detected errors

        :type reseller: parallels.core.target_data_model.Reseller
        :rtype: int, list[str]
        """
        reseller_command_arguments = [
            PleskCommandArgumentSimple('company', 'company', lambda _reseller: _reseller.company),
            PleskCommandArgumentSimple('contact name', 'name', lambda _reseller: join_nonempty_strs([
                _reseller.personal_info.first_name,
                _reseller.personal_info.last_name
            ])),
            PleskCommandArgumentSimple('e-mail', 'email', lambda _reseller: _reseller.personal_info.email),
            PleskCommandArgumentSimple('address', 'address', lambda _reseller: _reseller.personal_info.address),
            PleskCommandArgumentSimple('city', 'city', lambda _reseller: _reseller.personal_info.city),
            # Note: country must prepend state and ZIP code, because Plesk validation rules for these fields
            # depend on country
            PleskCommandArgumentSimple('country', 'country', lambda _reseller: _reseller.personal_info.country_code),
            PleskCommandArgumentSimple('state', 'state', lambda _reseller: _reseller.personal_info.state),
            PleskCommandArgumentSimple('ZIP code', 'zip', lambda _reseller: _reseller.personal_info.postal_code),
            PleskCommandArgumentSimple('phone', 'phone', lambda _reseller: _reseller.personal_info.primary_phone),
            PleskCommandArgumentSimple('fax', 'fax', lambda _reseller: _reseller.personal_info.fax),
            PleskCommandArgumentSimple('creation date', 'creation-date', lambda _reseller: _reseller.creation_date),
            PleskCommandArgumentSimple('service plan', 'service-plan', lambda _reseller: _reseller.plan_name),
            PleskCommandArgumentSimple('description', 'description', lambda _reseller: _reseller.description),
            PleskPasswordArgument()
        ]

        warnings = []
        additional_info_fields = None

        try:
            creator = PleskObjectCreatorAtOnce('reseller', reseller_command_arguments)
            creator.create(reseller)
        except Exception:
            logger.debug(messages.FAILED_TO_CREATE_RESELLER_AT_ONCE)
            logger.exception()
            creator = PleskObjectCreatorPartial('reseller', reseller_command_arguments)
            failed_fields = creator.create(reseller)

            if len(failed_fields) > 0:
                additional_info_fields, error_message_fields = format_failed_fields_list(failed_fields, reseller)
                warnings.append(safe_format(messages.FAILED_TO_SET_RESELLER_FIELDS,
                    login=reseller.login, fields=error_message_fields
                ))

        reseller_entity = self.get_by_username(reseller.login)
        if reseller_entity is None:
            raise MigrationError(safe_format(messages.FAILED_TO_GET_RESELLER_ID, login=reseller.login))

        try:
            self._set_reseller_limits_and_permissions(reseller)
        except Exception as e:
            logger.exception()
            warnings.append(safe_format(
                messages.FAILED_TO_SET_RESELLER_PREFS,
                login=reseller.login, reason=e
            ))

        # update reseller's locate as separate action, so missing locales do not block migration
        self._update_locale(reseller, warnings)

        self._update_info(reseller.login, reseller.personal_info, warnings, additional_info_fields)

        return reseller_entity.reseller_id, warnings

    def set_external_id(self, username, external_id):
        """Set External ID for given reseller in target Plesk

        :type username: str
        :type external_id: str
        """
        command = ResellerExternalIdCli(self.plesk_cli_runner, username, external_id)
        command.run()

    def _set_reseller_limits_and_permissions(self, reseller):
        """Set limits and permissions for specified existing reseller in target Plesk

        :type reseller: parallels.core.target_data_model.Reseller
        """
        command = ResellerPrefUpdateCli(
            self.plesk_cli_runner,
            reseller,
            self._get_reseller_permission_descriptor(),
            self._get_reseller_limit_descriptor()
        )
        command.run()

    @cached
    def _get_reseller_permission_descriptor(self):
        """Retrieve descriptor of available reseller permissions on target Plesk

        :rtype: parallels.plesk.utils.xml_rpc.plesk.core.Descriptor
        """
        request = plesk_ops.ResellerOperator.GetPermissionDescriptor(
            filter=plesk_ops.ResellerOperator.FilterEmpty()
        )
        result = Registry.get_instance().get_context().conn.target.plesk_api().send(request)
        return result.data

    @cached
    def _get_reseller_limit_descriptor(self):
        """Retrieve descriptor of available reseller limits on target Plesk

        :rtype: parallels.plesk.utils.xml_rpc.plesk.core.Descriptor
        """
        request = plesk_ops.ResellerOperator.GetLimitDescriptor(
            filter=plesk_ops.ResellerOperator.FilterEmpty()
        )
        result = Registry.get_instance().get_context().conn.target.plesk_api().send(request)
        return result.data

    def _update_locale(self, reseller, warnings):
        """Update locale of given reseller in target Plesk

        :type reseller: parallels.core.target_data_model.Reseller
        :type warnings: list[str]
        """
        if reseller.personal_info.locale is None:
            return

        try:
            command = ResellerPrefUpdateLocaleCli(self.plesk_cli_runner, reseller.login, reseller.personal_info.locale)
            command.run()
        except Exception as e:
            warnings.append(messages.IMPORT_SET_LOCALE_FAILED.format(
                locale=reseller.personal_info.locale,
                error=safe_string_repr(e).strip()
            ))

    def _update_info(self, username, personal_info, warnings, append_to_additional_info=None):
        """Update IM, IM type and comment fields for user (reseller, client)

        If append_additional_info field is set, this data is appended to the end of comment field.

        :type username: str|unicode
        :type personal_info: parallels.core.target_data_model.PersonalInfo
        :type warnings: list[str|unicode]
        :type append_to_additional_info: str | unicode | None
        """
        try:
            command = UserUpdateInfoCli(self.plesk_cli_runner, username, personal_info, append_to_additional_info)
            command.run()
        except Exception as e:
            message = messages.FAILED_TO_SET_RESELLER_CONTACT_DATA
            warnings.append(message.format(login=username, reason=safe_string_repr(e).strip()))
