import os
from xml.etree import ElementTree

from parallels.core import get_logger
from parallels.core.runners.base import BaseRunner
from parallels.core.runners.entities import ExecutionOptions
from parallels.core.utils.common import safe_string_repr
from parallels.core.utils.common.xml import elem, text_elem, xml_to_string_pretty
from parallels.plesk.source.plesk import messages

logger = get_logger(__name__)


class EncryptedDataItem(object):
    """Class describing a single data item which should be decrypted

    ID object can be any. As example of usage we can iterate over objects of this class and
    call data setter for each ID object to set the decrypted in XML or whatever else.
    Description can be used for logging and debugging.

    :type _id_object: object
    :type _description: str | unicode
    :type _value: str | unicode
    """
    def __init__(self, id_object, description, value):
        """
        :type id_object: object
        :type description: str | unicode
        :type value: str | unicode
        """
        self._id_object = id_object
        self._description = description
        self._value = value

    @property
    def id_object(self):
        return self._id_object

    @property
    def description(self):
        return self._description

    @property
    def value(self):
        return self._value

    def get_xml_node(self):
        """Get XML node which should be written to

        :rtype: xml.etree.ElementTree.Element
        """
        raise NotImplementedError()

    def get_decrypted_value(self, node):
        """
        :type node: xml.etree.ElementTree.Element
        """
        raise NotImplementedError()

    def validate_result(self, node):
        """Check if decryption worked correctly

        :type node: xml.etree.ElementTree.Element
        :rtype: bool
        """
        return True


class EncryptedPasswordItem(EncryptedDataItem):
    def get_xml_node(self):
        """Get path where decrypt utility expects this kind of encrypted data

        :rtype: list[str | unicode]
        """
        return text_elem(
            'password',  # password node
            self.value,  # password
            {
                'type': 'sym',  # required for decryption
            }
        )

    def get_decrypted_value(self, node):
        """
        :type node: xml.etree.ElementTree.Element
        """
        return node.findtext('password')

    def validate_result(self, node):
        """Whether we should check that 'type' attribute of decrypted node becomes 'plain'

        :type node: xml.etree.ElementTree.Element
        :rtype: bool
        """
        return node.find('password').get('type') == 'plain'


class EncryptedApsApplicationApscItem(EncryptedDataItem):
    def get_xml_node(self):
        """Get path where decrypt utility expects this kind of encrypted data

        :rtype: list[str | unicode]
        """
        return elem(
            'phosting', [
                elem('applications', [
                    elem('sapp-installed', [
                        text_elem('sapp-apsc', self.value)
                    ])
                ])
            ]
        )

    def get_decrypted_value(self, node):
        """
        :type node: xml.etree.ElementTree.Element
        """
        return node.findtext('phosting/applications/sapp-installed/sapp-apsc')


class EncryptedOdbcDsnConnectionStringItem(EncryptedDataItem):
    def get_xml_node(self):
        """Get path where decrypt utility expects this kind of encrypted data

        :rtype: list[str | unicode]
        """
        return elem(
            'domain', [
                elem('odbc-dsn', [
                    text_elem('connection-string', self.value)
                ])
            ]
        )

    def get_decrypted_value(self, node):
        """
        :type node: xml.etree.ElementTree.Element
        """
        return node.findtext('domain/odbc-dsn/connection-string')


class DecryptedDataItem(object):
    """Class describing a single item with decrypted value
    Id object can be any
    Description can be used for logging and debugging

    :type _id_object: object
    :type _description: str | unicode
    :type _value: str | unicode
    """
    def __init__(self, id_object, description, value):
        """
        :type id_object: object
        :type description: str | unicode
        :type value: str | unicode
        """
        self._id_object = id_object
        self._description = description
        self._value = value

    @property
    def id_object(self):
        return self._id_object

    @property
    def description(self):
        return self._description

    @property
    def value(self):
        return self._value


class DecryptedDataItemError(object):
    """Class describing an error of decrypting of a single item with encrypted value
    Id object can be any
    Description can be used for logging and debugging

    :type _id_object: object
    :type _description: str | unicode
    :type _error_message: str | unicode
    """
    def __init__(self, id_object, description, error_message):
        """
        :type id_object: object
        :type description: str | unicode
        :type error_message: str | unicode
        """
        self._id_object = id_object
        self._description = description
        self._error_message = error_message

    @property
    def id_object(self):
        return self._id_object

    @property
    def description(self):
        return self._description

    @property
    def error_message(self):
        return self._error_message


class PleskDecryptor(object):
    """
    :type _server: parallels.core.connections.plesk_server.PleskServer
    """

    def __init__(self, server):
        """
        :type server: parallels.core.connections.plesk_server.PleskServer
        """
        self._server = server

    def decrypt_data(self, data_items):
        """
        :type data_items: list[EncryptedDataItem]
        :rtype tuple[list[DecryptedDataItem], list[DecryptedDataItemError]]
        """
        decrypted_data_items = []
        decrypted_data_errors = []
        if not data_items:
            return decrypted_data_items, decrypted_data_errors

        try:
            # First, try to decrypt data items as a bulk
            logger.fdebug(messages.DATA_ITEMS_BULK_DECRYPTION)
            data_items, errors = self.decrypt_data_unsafe(data_items)
            decrypted_data_items.extend(data_items)
            decrypted_data_errors.extend(errors)
        except Exception:
            logger.exception()
            logger.fdebug(messages.DATA_BULK_DECRYPTION_FAILURE)

            # In case of error, try to decrypt each item individually
            for data_item in data_items:
                try:
                    logger.fdebug(messages.DATA_DECRYPTION, description=data_item.description)
                    data_items, errors = self.decrypt_data_unsafe([data_item])
                    decrypted_data_items.extend(data_items)
                    decrypted_data_errors.extend(errors)
                except Exception as e:
                    logger.exception()
                    logger.fdebug(messages.DATA_DECRYPTION_FAILURE, description=data_item.description)

                    decrypted_data_errors.append(
                        DecryptedDataItemError(
                            id_object=data_item.id_object,
                            description=data_item.description,
                            error_message=safe_string_repr(e)
                        )
                    )
        return decrypted_data_items, decrypted_data_errors

    def decrypt_data_unsafe(self, data_items):
        """
        :type data_items: list[EncryptedDataItem]
        :rtype tuple[list[DecryptedDataItem], list[DecryptedDataItemError]]
        """
        decrypted_data_items = []
        decrypted_data_errors = []
        if not data_items:
            return decrypted_data_items, decrypted_data_errors

        # XML template
        data_xml_tree = ElementTree.ElementTree(
            elem(
                'migration-dump',  # root node
                [
                    elem('item', [data_item.get_xml_node()]) for data_item in data_items
                ]
            )
        )

        with self._server.runner() as runner:
            assert isinstance(runner, BaseRunner)
            data_xml_file = self._server.get_session_file_path('data.xml')
            runner.upload_file_content(data_xml_file, xml_to_string_pretty(data_xml_tree))
            try:
                command, arguments = self._decrypt_data_command(data_xml_file)
                runner.execute_command(command, arguments, ExecutionOptions(log_output=False))
                data_file_content = runner.get_file_contents(data_xml_file)
                data_root_node = ElementTree.fromstring(data_file_content)

                for index, data_item in enumerate(data_items):
                    if data_item.validate_result(data_root_node[index]):
                        decrypted_data_items.append(
                            DecryptedDataItem(
                                id_object=data_item.id_object,
                                description=data_item.description,
                                value=data_item.get_decrypted_value(data_root_node[index])
                            )
                        )
                    else:
                        decrypted_data_errors.append(
                            DecryptedDataItemError(
                                id_object=data_item.id_object,
                                description=data_item.description,
                                error_message=messages.FAILED_TO_FIND_DECRYPTED_DATA
                            )
                        )
            finally:
                try:
                    runner.remove_file(data_xml_file)
                except:
                    logger.exception()

        return decrypted_data_items, decrypted_data_errors

    def _decrypt_data_command(self, data_xml_file):
        """
        :type data_xml_file: str | unicode
        :rtype: tuple[str|unicode, dict[str|unicode, str|unicode]]
        """
        command = ""
        arguments = dict()

        if self._server.is_windows():
            command += '{php_bin} -dauto_prepend_file="" {backup_encrypt_php}'
            arguments.update(dict(
                php_bin=os.path.join(self._server.plesk_dir, 'admin', 'bin', 'php'),
                backup_encrypt_php=os.path.join(
                    self._server.plesk_dir, 'admin', 'plib', 'cu', 'backup_encrypt.php'
                )
            ))
        else:
            command += 'unset PLESK_BACKUP_PASSWORD && '
            if self._server.plesk_version < (11, 1, 0):
                command += '{backup_encrypt_php}'
                arguments['backup_encrypt_php'] = os.path.join(
                    self._server.plesk_dir, 'admin', 'plib', 'api-cli', 'backup_encrypt.php'
                )
            elif (11, 1, 0) <= self._server.plesk_version < (12, 5, 0):
                command += '{backup_encrypt}'
                arguments['backup_encrypt'] = os.path.join(self._server.plesk_dir, 'bin', 'backup_encrypt')
            else:
                command += '{backup_encrypt}'
                arguments['backup_encrypt'] = os.path.join(self._server.plesk_dir, 'admin', 'bin', 'backup_encrypt')

        command += ' --decrypt-by-plesk -single-file {accounts_xml_file}'
        arguments['accounts_xml_file'] = data_xml_file

        return command, arguments
