import os.path
import xml.etree.ElementTree as ElementTree
import re

from parallels.core.logging import get_logger
from parallels.core.actions.base.common_action import CommonAction
from parallels.core.registry import Registry
from parallels.core.reports.model.affected_object import AffectedObject
from parallels.core.reports.model.issue import Issue
from parallels.core.utils import plesk_utils
from parallels.core.utils.common import is_empty, open_no_inherit
from parallels.core.utils.common.xml import xml_to_string_pretty
from parallels.plesk import messages
from parallels.plesk.utils.plesk_components import get_available_components

logger = get_logger(__name__)


class CheckCapabilityAction(CommonAction):
    def get_description(self):
        return messages.ACTION_CHECK_CAPABILITY_DESCRIPTION

    def get_failure_message(self, global_context):
        """
        :type global_context: parallels.core.global_context.GlobalMigrationContext
        """
        return messages.ACTION_CHECK_CAPABILITY_FAILURE

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

        If action is critical and it failed, migration tool completely stops.
        Otherwise it proceeds to the next steps of migrations.

        :rtype: bool
        """
        return False

    def filter_action(self, global_context):
        """Check whether we should run this action or not.

        :type global_context: parallels.core.global_context.GlobalMigrationContext
        :rtype: bool
        """
        if Registry.get_instance().get_command_name() not in ['transfer-accounts', 'check']:
            # Check capability is actual only for actual migration ('transfer-accounts' command)
            # and pre-migration checks ('check' command). It does not makes sense to run this
            # action for other operations, like final content sync ('copy-content', 'copy-web-content',
            # 'copy-mail-content', etc), post-migration checks ('test-all', 'test-sites', etc) and so on,
            # as migration is already done and capability pre-checks are nonsense, just a waste of time.
            return False

        if (
            hasattr(global_context.options, 'skip_capability_checks') and
            global_context.options.skip_capability_checks
        ):
            return False

        return True

    def run(self, global_context):
        """
        :type global_context: parallels.core.global_context.GlobalMigrationContext
        """
        common_issue_messages = set()
        for source_id in global_context.conn.get_source_plesks():
            capability_dump_path = global_context.session_files.get_path_to_capability_dump(source_id)
            capability_check_report_path = global_context.session_files.get_path_to_capability_check_report(source_id)
            if not os.path.exists(capability_dump_path):
                raise Exception(messages.UNABLE_TO_FIND_CAPABILITY_DUMP % capability_dump_path)

            capability_dump_remote_path = global_context.conn.target.main_node_session_file_path(
                'capability_dump.%s.xml' % source_id
            )
            capability_check_report_remote_path = global_context.conn.target.main_node_session_file_path(
                'capability_check_report.%s.xml' % source_id
            )
            with global_context.conn.target.main_node_runner() as runner:
                runner.upload_file(capability_dump_path, capability_dump_remote_path)

            plesk_utils.check_capability(
                global_context.conn.target.plesk_server,
                capability_dump_remote_path,
                capability_check_report_remote_path
            )
            with global_context.conn.target.main_node_runner() as runner:
                if not runner.file_exists(capability_check_report_remote_path):
                    raise Exception(
                        messages.UNABLE_TO_FIND_CAPABILITY_CHECK_REPORT % capability_check_report_remote_path
                    )
                runner.get_file(capability_check_report_remote_path, capability_check_report_path)

            report_tree = ElementTree.parse(capability_check_report_path)
            self._adjust_components_in_tree(global_context, report_tree)

            capability_report_str = xml_to_string_pretty(report_tree)
            with open_no_inherit(capability_check_report_path, 'w') as fp:
                fp.write(capability_report_str)

            for message_node in report_tree.findall('.//message'):

                severity_node = message_node.find('severity')
                severity_string = severity_node.text if severity_node is not None else ''
                if severity_string == 'warning':
                    severity = Issue.SEVERITY_WARNING
                elif severity_string == 'error':
                    severity = Issue.SEVERITY_ERROR
                else:
                    severity = Issue.SEVERITY_INFO

                text_nodes = message_node.findall('text')
                texts = []
                if len(text_nodes) > 0:
                    for text_node in text_nodes:
                        if text_node.text is not None:
                            texts.append(text_node.text)
                description = '\n\r'.join(texts)
                if description == '':
                    continue

                if message_node.findtext('resolutionDescription/componentName') in {'perl', 'python'}:
                    # Perl/Python not installed on target Windows server.
                    #
                    # To avoid multiple reporting, such common for Plesk >= 12.5 issues are processed at:
                    # - parallels.plesk.actions.hosting_settings.convert.
                    #     remove_missing_components.RemoveMissingComponents
                    # - parallels.plesk.actions.hosting_settings.check.
                    #     check_missing_components.CheckMissingComponents
                    continue

                resolution_text_node = message_node.find('resolutionDescription/text')
                solution = resolution_text_node.text if resolution_text_node is not None else None

                object_nodes = message_node.findall('objects-list/object')
                if len(object_nodes) == 0 and description not in common_issue_messages:
                    # report common issue, not related to specific object
                    global_context.pre_check_report.add_issue(
                        'capability_%s_%s' % (severity, source_id),
                        severity, self._fix_description_text(description),
                        solution
                    )
                    common_issue_messages.add(description)
                else:
                    affected_objects = []

                    for object_node in object_nodes:
                        type_node = object_node.find('type')
                        if type_node is None or type_node.text != 'domain':
                            # skip all issues, not related to domains
                            continue
                        name_node = object_node.find('name')
                        if name_node is None:
                            continue
                        subscription_name = name_node.text
                        if not global_context.has_subscription(subscription_name):
                            continue

                        affected_objects.append(AffectedObject(
                            object_type=AffectedObject.TYPE_SUBSCRIPTION,
                            name=subscription_name
                        ))

                    global_context.pre_check_report.add_issue(
                        'capability_%s_%s' % (severity, source_id),
                        severity, self._fix_description_text(description),
                        solution, affected_objects=affected_objects
                    )

    @staticmethod
    def _fix_description_text(text):
        """Fix description text of the issue by adding additional spaces in lists

        :type text: str | unicode
        :rtype: str | unicode
        """
        return re.sub(r',(\S)', r', \1', text)

    @staticmethod
    def _adjust_components_in_tree(global_context, report_tree):
        """Adjust components that can not be installed in the capability report.

        For example, on certain recent Linux OSes like CentOS 7, mod_python is not available.
        That action is required to remove "Install" button from UI for such components.

        :type global_context: parallels.core.global_context.GlobalMigrationContext
        :type report_tree: xml.etree.ElementTree.ElementTree
        :rtype: None
        """
        target_plesk_server = global_context.conn.target.plesk_server
        target_plesk_version = target_plesk_server.plesk_version

        for message_node in report_tree.findall('.//message'):
            if message_node.findtext('resolutionDescription/type') == 'install':

                # python, perl
                component_name = message_node.findtext('resolutionDescription/componentName')
                is_windows = global_context.conn.target.is_windows
                can_not_install = (
                    (
                        not is_windows
                        and component_name in {'mod_python', 'mod_perl'}
                        and component_name not in get_available_components(target_plesk_server)
                    )
                )
                if can_not_install:
                    logger.finfo(messages.CAPABILITY_SET_COMPONENT_CANNOT_BE_INSTALLED, component_name=component_name)
                    message_node.find('resolutionDescription/type').text = 'nothing'
                    if not is_empty(message_node.findtext('resolutionDescription/text')):
                        message_node.find('resolutionDescription/text').text = ''

                # tomcat
                # tomcat was dropped in Plesk 17.8, but Plesk`s capability checker advises to install the component
                if target_plesk_version >= (17, 8) and component_name == 'tomcat':
                    logger.finfo(messages.CAPABILITY_SET_COMPONENT_CANNOT_BE_INSTALLED, component_name=component_name)
                    message_node.find('resolutionDescription/type').text = 'nothing'
                    if not is_empty(message_node.findtext('resolutionDescription/text')):
                        message_node.find('resolutionDescription/text').text = \
                            messages.JAVA_APPLICATIONS_ARE_NO_LONGER_SUPPORTED

