from parallels.core import get_logger
from parallels.core.actions.base.common_action import CommonAction
from parallels.core.actions.utils.logging_properties import LoggingProperties
from parallels.core.dump.entity.password import Password
from parallels.core.reports.model.issue import Issue
from parallels.core.utils.common import between_inclusive, safe_format, \
    generate_random_password, format_multiline_list, is_empty
from parallels.core.utils.common_constants import ADMIN_USERNAME
from parallels.core.utils.entity import Entity
from parallels.plesk.source.plesk import messages
from parallels.plesk.source.plesk.hosting_description.plesk_decryptor import EncryptedPasswordItem, \
    PleskDecryptor, DecryptedDataItem, DecryptedDataItemError, EncryptedApsApplicationApscItem, \
    EncryptedOdbcDsnConnectionStringItem

logger = get_logger(__name__)


class DecryptData(CommonAction):
    """Get all passwords with type 'sym' from dump and decrypt them
    """
    def get_description(self):
        """Get short description of action as string

        :rtype: str | unicode
        """
        return messages.ACTION_DECRYPT_DATA_DESCRIPTION

    def get_failure_message(self, global_context):
        """Get message for situation when action failed

        :type global_context: parallels.core.global_context.GlobalMigrationContext
        :rtype: str | unicode
        """
        return messages.ACTION_DECRYPT_DATA_FAILURE

    def get_logging_properties(self):
        """Get how action should be logged to migration tools end-user

        :rtype: parallels.core.actions.utils.logging_properties.LoggingProperties
        """
        return LoggingProperties(compound=False)

    def is_critical(self):
        """If action is critical or not

        If action is critical and it failed for a subscription, migration tool
        won't run the next operations for the subscription.

        :rtype: bool
        """
        return False

    def filter_action(self, global_context):
        """Check whether we should run this action or not. By default True - action should be executed.

        :type global_context: parallels.plesk.source.plesk.global_context.PleskGlobalMigrationContext
        :rtype: bool
        """
        for source_id, _ in global_context.conn.get_source_plesks().items():
            source_server = global_context.conn.get_source_node(source_id)

            # No encrypted passwords in Plesk versions below 11.0
            # Another dump mechanism in Plesk versions above 12.5.30
            if between_inclusive(source_server.plesk_version, (11, 0), (12, 5, 30)):
                return True

        return False

    def run(self, global_context):
        """
        :type global_context: parallels.plesk.source.plesk.global_context.PleskGlobalMigrationContext
        """
        for source_id, settings in global_context.conn.get_source_plesks().items():
            source_server = global_context.conn.get_source_node(source_id)

            # No encrypted passwords in Plesk versions below 11.0
            # Plesk 17 and above creating a hosting description file with already decrypted passwords
            if between_inclusive(source_server.plesk_version, (11, 0), (12, 5, 30)):
                backup = global_context.load_raw_dump(settings)
                self._decrypt_data_for_server(backup, source_server, global_context)
                backup.save_to_the_same_file()

    def _decrypt_data_for_server(self, backup, source_server, global_context):
        """
        :type backup: parallels.core.dump.dump.PleskBackupSource11
        :type source_server: parallels.core.connections.plesk_server.PleskServer
        :type global_context: parallels.plesk.source.plesk.global_context.PleskGlobalMigrationContext
        """
        encrypted_password_items = self._get_all_encrypted_passwords(backup)
        encrypted_apsc_data_items = self._get_encrypted_apsc_data_objects(backup)
        encrypted_odbc_dsn_items = self._get_encrypted_odbc_dsn_objects(backup)

        decryptor = PleskDecryptor(source_server)
        decrypted_items, decrypted_errors = decryptor.decrypt_data(
            encrypted_password_items + encrypted_apsc_data_items + encrypted_odbc_dsn_items
        )

        list_failed_passwords = []
        # Update objects with decrypted passwords
        for decrypted_item in decrypted_items:
            assert isinstance(decrypted_item, DecryptedDataItem)

            if isinstance(decrypted_item.id_object, PasswordObject):
                # if password decrypted to None, generate password
                if decrypted_item.value is None:
                    logger.fwarn(
                        messages.FAILED_TO_DECRYPT_ACCOUNT_PASSWORD,
                        object_description=decrypted_item.id_object.owner_description
                    )
                    decrypted_item.id_object.owner.password = Password(
                        password_text=generate_random_password(),
                        password_type='plain'
                    )
                    list_failed_passwords.append(decrypted_item.description)
                    continue
                # if password decrypted well
                decrypted_item.id_object.owner.password = Password(
                    password_text=decrypted_item.value,
                    password_type='plain'
                )
            elif isinstance(decrypted_item.id_object, ApsControllerDataObject):
                decrypted_item.id_object.aps_application.sapp_apsc = decrypted_item.value
            elif isinstance(decrypted_item.id_object, OdbcDsnObject):
                decrypted_item.id_object.odbc_dsn.connection_string = decrypted_item.value

        # Show message for failed decryption actions and generate passwords for related objects
        for decrypted_error in decrypted_errors:
            assert isinstance(decrypted_error, DecryptedDataItemError)

            if isinstance(decrypted_error.id_object, PasswordObject):
                # If password decryption is failed, generate password
                logger.fwarn(
                    messages.FAILED_TO_DECRYPT_ACCOUNT_PASSWORD,
                    object_description=decrypted_error.description
                )
                decrypted_error.id_object.password = Password(
                    password_text=generate_random_password(),
                    password_type='plain'
                )
                list_failed_passwords.append(decrypted_error.description)
            elif isinstance(decrypted_error.id_object, ApsControllerDataObject):
                logger.fwarn(
                    messages.FAILED_TO_DECRYPT_APSC_DATA,
                    application=decrypted_error.id_object.aps_application.name,
                    subscription=decrypted_error.id_object.subscription.name
                )
                global_context.pre_check_report.add_issue(
                    'apsc_data_decryption_failure',
                    Issue.SEVERITY_WARNING,
                    safe_format(
                        messages.FAILED_TO_DECRYPT_APSC_DATA,
                        application=decrypted_error.id_object.aps_application.name,
                        subscription=decrypted_error.id_object.subscription.name
                    )
                )
            elif isinstance(decrypted_error.id_object, OdbcDsnObject):
                logger.fwarn(
                    messages.FAILED_TO_DECRYPT_ODBC_DSN,
                    subscription=decrypted_error.id_object.subscription.name,
                    odbc_dsn_name=decrypted_error.id_object.odbc_dsn.name
                )
                global_context.pre_check_report.add_issue(
                    'odbc_dsn_decryption_failure',
                    Issue.SEVERITY_WARNING,
                    safe_format(
                        messages.FAILED_TO_DECRYPT_ODBC_DSN,
                        subscription=decrypted_error.id_object.subscription.name,
                        odbc_dsn_name=decrypted_error.id_object.odbc_dsn.name
                    )
                )

        # Add message with a list of affected objects to the final report
        if list_failed_passwords:
            global_context.pre_check_report.add_issue(
                'password_decryption_failure',
                Issue.SEVERITY_WARNING,
                safe_format(
                    messages.FAILED_TO_DECRYPT_PASSWORDS,
                    list_objects=format_multiline_list(list_failed_passwords)
                )
            )

        # Need to remove encryption properties from all XML files
        # Options exist in dump version 11.1 and higher
        if backup.container.dump_version >= (11, 1):
            backup.remove_encryption_properties()

    @staticmethod
    def _get_encrypted_apsc_data_objects(backup):
        """
        :type backup: parallels.core.dump.dump.PleskBackupSource11
        :rtype: list[parallels.plesk.source.plesk.hosting_description.plesk_decryptor.EncryptedPasswordItem]
        """
        objects = []

        for subscription in backup.iter_all_subscriptions():
            for aps_application in subscription.get_aps_applications():
                if not is_empty(aps_application.sapp_apsc):
                    objects.append(
                        EncryptedApsApplicationApscItem(
                            id_object=ApsControllerDataObject(subscription, aps_application),
                            description=safe_format(
                                messages.APSC_DATA_DESCRIPTION,
                                application=aps_application.name,
                                subscription=subscription.name
                            ),
                            value=aps_application.sapp_apsc
                        )
                    )

        return objects

    @staticmethod
    def _get_encrypted_odbc_dsn_objects(backup):
        """
        :type backup: parallels.core.dump.dump.PleskBackupSource11
        :rtype: list[parallels.plesk.source.plesk.hosting_description.plesk_decryptor.EncryptedOdbcDsnConnectionStringItem]
        """
        objects = []

        for subscription in backup.iter_all_subscriptions():
            for odbc_dsn in subscription.odbc_dsns:
                if not is_empty(odbc_dsn.connection_string):
                    objects.append(
                        EncryptedOdbcDsnConnectionStringItem(
                            id_object=OdbcDsnObject(subscription, odbc_dsn),
                            description=safe_format(
                                messages.ODBC_DSN_CONNECTION_STRING,
                                subscription=subscription.name
                            ),
                            value=odbc_dsn.connection_string
                        )
                    )

        return objects

    def _get_all_encrypted_passwords(self, backup):
        """ Get passwords with type 'sym' from dump
        :type backup: parallels.core.dump.dump.PleskBackupSource11
        :rtype: list[parallels.plesk.source.plesk.hosting_description.plesk_decryptor.EncryptedPasswordItem]
        """
        encrypted_password_items = []

        # database_servers
        encrypted_password_items.extend(
            self._get_encrypted_passwords_for_database_servers(backup)
        )

        # admin aux_users
        for admin_auxiliary_user in backup.iter_admin_auxiliary_users():
            if admin_auxiliary_user.password.password_type != 'sym':
                continue
            encrypted_password_items.append(
                self._get_encrypted_password_item_for_aux_user(admin_auxiliary_user, ADMIN_USERNAME)
            )

        # admin subscriptions
        for admin_subscription in backup.iter_admin_subscriptions():
            encrypted_password_items.extend(
                self._get_all_encrypted_passwords_in_subscription(admin_subscription, ADMIN_USERNAME)
            )

        # admin clients
        for client in backup.iter_clients():
            encrypted_password_items.extend(
                self._get_all_encrypted_passwords_of_client(client, ADMIN_USERNAME)
            )

        for reseller in backup.iter_resellers():
            # resellers
            if reseller.password.password_type == 'sym':
                owner_description = safe_format(messages.RESELLER_DESCRIPTION, login=reseller.login)
                encrypted_password_items.append(
                    EncryptedPasswordItem(
                        id_object=PasswordObject(owner=reseller, owner_description=owner_description),
                        description=safe_format(messages.PASSWORD_DESCRIPTION, owner_description=owner_description),
                        value=reseller.password.password_text,
                    )
                )

            # reseller aux_users
            for auxiliary_user in reseller.auxiliary_users:
                if auxiliary_user.password.password_type != 'sym':
                    continue
                encrypted_password_items.append(
                    self._get_encrypted_password_item_for_aux_user(auxiliary_user, reseller.login)
                )

            # reseller subscriptions
            for subscription in reseller.subscriptions:
                encrypted_password_items.extend(
                    self._get_all_encrypted_passwords_in_subscription(subscription, reseller.login)
                )

            # reseller clients
            for client in reseller.clients:
                encrypted_password_items.extend(
                    self._get_all_encrypted_passwords_of_client(client, reseller.login)
                )

        return encrypted_password_items

    @staticmethod
    def _get_encrypted_passwords_for_database_servers(backup):
        """ Get passwords with type 'sym' for database servers
        :type backup: parallels.core.dump.dump.PleskBackupSource11
        :rtype: list[parallels.plesk.source.plesk.hosting_description.plesk_decryptor.EncryptedPasswordItem]
        """
        encrypted_password_items = []

        for database_server in backup.iter_db_servers():
            if database_server.password.password_type != 'sym':
                continue
            owner_description = safe_format(
                messages.DATABASE_SERVER_DESCRIPTION, type=database_server.dbtype,
                host=database_server.host, port=str(database_server.port)
            )
            encrypted_password_items.append(
                EncryptedPasswordItem(
                    id_object=PasswordObject(owner=database_server, owner_description=owner_description),
                    description=safe_format(messages.PASSWORD_DESCRIPTION, owner_description=owner_description),
                    value=database_server.password.password_text,
                )
            )

        return encrypted_password_items

    def _get_all_encrypted_passwords_of_client(self, client, owner_name):
        """ Get passwords with type 'sym' for client and objects under client
        :type client: parallels.core.dump.data_model.Client
        :type owner_name: str | unicode
        :rtype: list[parallels.plesk.source.plesk.hosting_description.plesk_decryptor.EncryptedPasswordItem]
        """
        encrypted_password_items = []

        # clients
        if client.password.password_type == 'sym':
            owner_description = safe_format(messages.CLIENT_DESCRIPTION, login=client.login, owner_name=owner_name)
            encrypted_password_items.append(
                EncryptedPasswordItem(
                    id_object=PasswordObject(owner=client, owner_description=owner_description),
                    description=safe_format(messages.PASSWORD_DESCRIPTION, owner_description=owner_description),
                    value=client.password.password_text,
                )
            )

        # clients's aux_users
        for auxiliary_user in client.auxiliary_users:
            if auxiliary_user.password.password_type != 'sym':
                continue
            encrypted_password_items.append(
                self._get_encrypted_password_item_for_aux_user(auxiliary_user, client.login)
            )

        # clients's subscriptions
        for subscription in client.subscriptions:
            encrypted_password_items.extend(
                self._get_all_encrypted_passwords_in_subscription(subscription, client.login)
            )

        return encrypted_password_items

    @staticmethod
    def _get_all_encrypted_passwords_in_subscription(subscription, owner_name):
        """ Get passwords with type 'sym' for subscription and objects under subscription
        :type subscription: parallels.core.dump.data_model.Subscription
        :type owner_name: str | unicode
        :rtype: list[parallels.plesk.source.plesk.hosting_description.plesk_decryptor.EncryptedPasswordItem]
        """
        encrypted_password_items = []

        # sysusers
        for sysuser in subscription.iter_system_users():
            if sysuser.password.password_type != 'sym':
                continue
            owner_description = safe_format(
                messages.SYS_USER_DESCRIPTION,
                name=sysuser.name, subscription_name=subscription.name, owner_name=owner_name
            )
            encrypted_password_items.append(
                EncryptedPasswordItem(
                    id_object=PasswordObject(owner=sysuser, owner_description=owner_description),
                    description=safe_format(messages.PASSWORD_DESCRIPTION, owner_description=owner_description),
                    value=sysuser.password.password_text,
                )
            )

        # mail_accounts
        for mail_account in subscription.iter_all_mail_accounts():
            if mail_account.password.password_type != 'sym':
                continue
            owner_description = safe_format(
                messages.MAIL_ACCOUNT_DESCRIPTION, mail_account_name=mail_account.name,
                subscription_name=subscription.name, owner_name=owner_name
            )
            encrypted_password_items.append(
                EncryptedPasswordItem(
                    id_object=PasswordObject(owner=mail_account, owner_description=owner_description),
                    description=safe_format(messages.PASSWORD_DESCRIPTION, owner_description=owner_description),
                    value=mail_account.password.password_text,
                )
            )

        # database_users
        for database_user in subscription.iter_all_database_users():
            if database_user.password.password_type != 'sym':
                continue
            owner_description = safe_format(
                messages.DATABASE_USER_DESCRIPTION, database_user_name=database_user.name,
                subscription_name=subscription.name, owner_name=owner_name
            )
            encrypted_password_items.append(
                EncryptedPasswordItem(
                    id_object=PasswordObject(owner=database_user, owner_description=owner_description),
                    description=safe_format(messages.PASSWORD_DESCRIPTION, owner_description=owner_description),
                    value=database_user.password.password_text,
                )
            )

        # protected_directories_users
        for domain in subscription.iter_domains():
            for protected_dir in domain.iter_protected_dirs():
                for user in protected_dir.iter_users():
                    if user.password.password_type != 'sym':
                        continue
                    owner_description = safe_format(
                        messages.PD_USER_DESCRIPTION,
                        protected_directory=protected_dir.name,
                        pd_user_name=user.name, subscription_name=subscription.name, owner_name=owner_name
                    )
                    encrypted_password_items.append(
                        EncryptedPasswordItem(
                            id_object=PasswordObject(owner=user, owner_description=owner_description),
                            description=safe_format(messages.PASSWORD_DESCRIPTION, owner_description=owner_description),
                            value=user.password.password_text,
                        )
                    )

        return encrypted_password_items

    @staticmethod
    def _get_encrypted_password_item_for_aux_user(aux_user, owner_name):
        """ Get passwords with type 'sym' for client and objects under client
        :type aux_user: parallels.core.dump.data_model.AuxiliaryUser
        :type owner_name: str | unicode
        :rtype: list[parallels.plesk.source.plesk.hosting_description.plesk_decryptor.EncryptedPasswordItem]
        """
        owner_description = safe_format(
            messages.AUX_USER_DESCRIPTION, aux_user_name=aux_user.name, owner_name=owner_name
        )
        return EncryptedPasswordItem(
            id_object=PasswordObject(owner=aux_user, owner_description=owner_description),
            description=safe_format(messages.PASSWORD_DESCRIPTION, owner_description=owner_description),
            value=aux_user.password.password_text,
        )


class PasswordObject(Entity):
    def __init__(self, owner, owner_description):
        self._owner = owner
        self._owner_description = owner_description

    @property
    def owner(self):
        return self._owner

    @property
    def owner_description(self):
        return self._owner_description


class ApsControllerDataObject(Entity):
    def __init__(self, subscription, aps_application):
        """
        :type subscription: parallels.core.dump.data_model.Subscription
        :type aps_application: parallels.core.dump.entity.aps_application.ApsApplication
        """
        self._subscription = subscription
        self._aps_application = aps_application

    @property
    def subscription(self):
        """
        :rtype: parallels.core.dump.data_model.Subscription
        """
        return self._subscription

    @property
    def aps_application(self):
        """
        :rtype: parallels.core.dump.entity.aps_application.ApsApplication
        """
        return self._aps_application


class OdbcDsnObject(Entity):
    def __init__(self, subscription, odbc_dsn):
        """
        :type subscription: parallels.core.dump.data_model.Subscription
        :type odbc_dsn: parallels.core.dump.entity.odbc_dsn.OdbcDsn
        """
        self._subscription = subscription
        self._odbc_dsn = odbc_dsn

    @property
    def subscription(self):
        """
        :rtype: parallels.core.dump.data_model.Subscription
        """
        return self._subscription

    @property
    def odbc_dsn(self):
        """
        :rtype: parallels.core.dump.entity.odbc_dsn.OdbcDsn
        """
        return self._odbc_dsn
