import collections
import copy
import itertools
import logging
import sys
import traceback
from xml.etree import ElementTree
from contextlib import contextmanager

from parallels.core import messages
from parallels.core.logging.binary import BinaryLogger, RecordSeverity
from parallels.core.logging.context import get_current_subscription_name
from parallels.core.registry import Registry
from parallels.core.utils.common import safe_format, safe_string_repr

XmlrpcHost = collections.namedtuple('XmlrpcHost', 'value')
XmlrpcMessage = collections.namedtuple('XmlrpcMessage', 'data')


def get_logger(name):
    """Create instance of fault-tolerant logger for specified module

    Typical usage:
    logger = get_logger(__name__)

    :type name: str | unicode
    :rtype: parallels.core.logging.loggers.MigratorLogger
    """
    return MigratorLogger(logging.getLogger(name))


class MigratorLogger(object):
    """Class that should be used in migrator for logging

    Comparing to standard Python loggers:
    - it is more fault-tolerant for formatting issues
    - it writes both text (for users) and binary logs (for GUI)

    All methods available to standard Python logger are here.
    There are additional methods for fault-tolerant logging:
    'finfo', 'fdebug', 'ferror', 'fwarn'.
    """
    def __init__(self, logger):
        self._logger = logger
        self._binary_logger = BinaryLogger.get_instance()

    @contextmanager
    def migrator_execution_context(self, *args, **kwargs):
        message = safe_format(*args, **kwargs)
        self._binary_logger.start_level(
            RecordSeverity.MIGRATOR_EXECUTION, message
        )
        try:
            yield
        finally:
            self._binary_logger.end_level()

    def info(self, msg, *args, **kwargs):
        """Log info-level message"""
        self._logger.info(msg, *args, **kwargs)
        self._binary_logger.add_record(
            RecordSeverity.INFO, self._format_positional_message(msg, args)
        )

    def warning(self, msg, *args, **kwargs):
        """Log warning-level message"""
        self._logger.warn(msg, *args, **kwargs)
        self._binary_logger.add_record(
            RecordSeverity.WARN, self._format_positional_message(msg, args)
        )

    def warn(self, msg, *args, **kwargs):
        """Log warning-level message"""
        self.warning(msg, *args, **kwargs)

    def debug(self, msg, *args, **kwargs):
        """Log debug-level message"""
        self._logger.debug(msg, *args, **kwargs)
        self._binary_logger.add_record(
            RecordSeverity.DEBUG, self._format_positional_message(msg, args)
        )

    def error(self, msg, *args, **kwargs):
        """Log error-level message"""
        self._logger.error(msg, *args, **kwargs)
        self._binary_logger.add_record(
            RecordSeverity.ERROR, self._format_positional_message(msg, args)
        )

    def exception(self):
        self._logger.debug(messages.LOG_EXCEPTION, exc_info=True)
        self._binary_logger.add_record(
            RecordSeverity.EXCEPTION, messages.LOG_EXCEPTION + u"\n" + safe_string_repr(traceback.format_exc())
        )

    def finfo(self, *args, **kwargs):
        """Log info-level message, with fault tolerant string formatting

        First positional argument is a format string. All the other arguments must be named arguments.
        Example:
            log.finfo('Create file {filename} for domain {domain}', filename='a.txt', domain='example.com')
        """
        message = safe_format(*args, **kwargs)
        self._logger.info(message)
        self._binary_logger.add_record(RecordSeverity.INFO, message)

    def fdebug(self, *args, **kwargs):
        """Log debug-level message, with fault tolerant string formatting

        First positional argument is a format string. All the other arguments must be named arguments.
        Example:
            log.finfo('Create file {filename} for domain {domain}', filename='a.txt', domain='example.com')
        """
        message = safe_format(*args, **kwargs)
        self._logger.debug(message)
        self._binary_logger.add_record(RecordSeverity.DEBUG, message)

    def ferror(self, *args, **kwargs):
        """Log error-level message, with fault tolerant string formatting

        First positional argument is a format string. All the other arguments must be named arguments.
        Example:
            log.finfo('Failed to create file {filename} for domain {domain}', filename='a.txt', domain='example.com')
        """
        message = safe_format(*args, **kwargs)
        self._logger.error(message)
        self._binary_logger.add_record(RecordSeverity.ERROR, message)

    def fwarn(self, *args, **kwargs):
        """Log warning-level message, with fault tolerant string formatting

        First positional argument is a format string. All the other arguments must be named arguments.
        Example:
            log.finfo('Failed to create file {filename} for domain {domain}', filename='a.txt', domain='example.com')
        """
        message = safe_format(*args, **kwargs)
        self._logger.warn(message)
        self._binary_logger.add_record(RecordSeverity.WARN, message)

    def start_info_level(self, *args, **kwargs):
        """Log info-level message starting new log level. All the next messages will be under that message

        First positional argument is a format string. All the other arguments must be named arguments.
        Example:
            log.start_info_level('Create file {filename} for domain {domain}', filename='a.txt', domain='example.com')
        """
        message = safe_format(*args, **kwargs)
        self._logger.info(message)
        self._binary_logger.start_level(RecordSeverity.INFO, message)

    def start_debug_level(self, *args, **kwargs):
        """Log debug-level message starting new log level. All the next messages will be under that message

        First positional argument is a format string. All the other arguments must be named arguments.
        Example:
            log.start_debug_level('Create file {filename} for domain {domain}', filename='a.txt', domain='example.com')
        """
        message = safe_format(*args, **kwargs)
        self._logger.debug(message)
        self._binary_logger.start_level(RecordSeverity.DEBUG, message)

    @contextmanager
    def info_level_context(self, *args, **kwargs):
        """Context manager to log info-level message starting new log level.

        All the messages inside that context ("with" statement) will be under that message.

        First positional argument is a format string. All the other arguments must be named arguments.
        Example:
          log.info_level_context('Create file {filename} for domain {domain}', filename='a.txt', domain='example.com')
        """
        self.start_info_level(*args, **kwargs)
        try:
            yield
        finally:
            self.end_level()

    @contextmanager
    def debug_level_context(self, *args, **kwargs):
        """Context manager to log debug-level message starting new log level.

        All the messages inside that context ("with" statement) will be under that message.

        First positional argument is a format string. All the other arguments must be named arguments.
        Example:
          log.debug_level_context('Create file {filename} for domain {domain}', filename='a.txt', domain='example.com')
        """
        self.start_debug_level(*args, **kwargs)
        try:
            yield
        finally:
            self.end_level()

    def end_level(self):
        """End log level

        :rtype: None
        """
        self._binary_logger.end_level()

    @contextmanager
    def debug_progress_context(self, *args, **kwargs):
        """Progress context with additional logging context at debug level"""
        with self.debug_level_context(*args, **kwargs):
            with self._progress_context(*args, **kwargs):
                yield

    @contextmanager
    def info_progress_context(self, *args, **kwargs):
        """Progress context with additional logging context at info level"""
        with self.info_level_context(*args, **kwargs):
            with self._progress_context(*args, **kwargs):
                yield

    @contextmanager
    def progress_context(self, *args, **kwargs):
        """Progress context without any logging to debug or info log - just update the progress"""
        with self._progress_context(*args, **kwargs):
            yield

    def __getattr__(self, attr):
        """Proxy all other requests to native Python logger

        :type attr: str | unicode
        """
        return getattr(self._logger, attr)

    @contextmanager
    def _progress_context(self, *args, **kwargs):
        message = safe_format(*args, **kwargs)
        current_subscription = get_current_subscription_name()
        if current_subscription is not None:
            context = Registry.get_instance().get_context()
            if context is not None:
                subscription_progress = context.progress.get_subscription(current_subscription)
                subscription_progress.details = message
                yield
                subscription_progress.details = None
            else:
                yield
        else:
            yield

    @staticmethod
    def _format_positional_message(msg, args):
        if len(args) == 0 or args is None:
            return msg
        else:
            return msg % args


class BleepingLogger(object):
    """Wipe out sensitive data before logging messages to a file.
    
    This object searches for log message arguments with non-primitive
    types(xml.elementtree, list, dictionary), removes sensitive data from them,
    converts the cleaned up arguments to string, then passes message with
    string arguments to logger.
    
    :type logger: parallels.core.logging.loggers.MigratorLogger
    """

    def __init__(self, logger, xml_serializer=None, context_length=10):
        self.logger = logger
        self.xml_serializer = xml_serializer
        self.context_length = int(context_length)
        if self.context_length < 0:
            self.log_context_length = 0

        self.xml_request_sensitivedata = ['.//passwd', './/password']
        self.cli_sensitive_arguments = ['-passwd']
        self.dict_cli_sensitive_arguments = ['passwd', 'password', 'src_password', 'src_passwd']
        self.xmlrpc_sensitive_fields = ['passwd', 'password']
        self.message_walk_maxdepth = 11
        self.dummy_text = '***hidden***'

    def fdebug(self, message, *args, **kwargs):
        self.logger.fdebug(message, *self._get_sane_args(args), **self._get_sane_kwargs(kwargs))

    @contextmanager
    def debug_level_context(self, message, *args, **kwargs):
        with self.logger.debug_level_context(message, *self._get_sane_args(args), **self._get_sane_kwargs(kwargs)):
            yield

    def _get_sane_args(self, args):
        """Remove sensitive data from log message arguments.
        
        :type args: list | tuple
        :rtype: list
        """
        return [self._get_clean_arg_value(arg) for arg in args]

    def _get_sane_kwargs(self, kwargs):
        """Remove sensitive data from log message arguments.
        
        :type kwargs: dict
        :rtype: dict
        """
        return {k: self._get_clean_arg_value(v) for k, v in kwargs.items()}

    def _get_clean_arg_value(self, arg_value):
        if isinstance(arg_value, ElementTree.ElementTree):
            return self._cleanup_xmltree(arg_value)
        elif isinstance(arg_value, XmlrpcMessage):
            return self._cleanup_xmlrpc_message(arg_value)
        else:
            return arg_value

    def _cleanup_xmltree(self, xmltree):
        sane_xmltree = copy.deepcopy(xmltree)
        sane_xmlstring = self._cleanup_api_message(sane_xmltree)
        short_xmlstring = self._wrap_multiline_string(sane_xmlstring)
        return short_xmlstring

    def _cleanup_api_message(self, xml):
        """Remove sensitive data from API message body; serialize it to string."""
        found_elements = map(xml.findall, self.xml_request_sensitivedata)
        for text_element_to_bleep in itertools.chain(*found_elements):
            text_element_to_bleep.text = self.dummy_text
        if sys.version_info.major >= 3:
            xmlstring = self.xml_serializer(xml)
        else:
            xmlstring = unicode(self.xml_serializer(xml), encoding='utf-8')
        return xmlstring

    def _cleanup_xmlrpc_message(self, message):
        """Remove sensitive data from an XML RPC parsed message."""
        def eraser(key, val):
            if key in self.xmlrpc_sensitive_fields:
                return self.dummy_text
            else:
                return val
        message_struct = copy.deepcopy(message.data)
        return self._walk_message_data(message_struct, eraser, 0) 

    def _walk_message_data(self, message, handler, level):
        """Recursively process elements of a Python nested dictionary."""
        if level > self.message_walk_maxdepth:
            # stop processing a too deeply nested struct
            return
        if isinstance(message, collections.Mapping):
            for argument, value in message.items():
                message[argument] = handler(argument, value)
                self._walk_message_data(value, handler, level + 1)
        elif isinstance(message, list) or isinstance(message, tuple):
            for item in message:
                self._walk_message_data(item, handler, level + 1)
        return message

    def _wrap_multiline_string(self, message):
        """Replace long multiline message body with ellipsis."""
        if not self.context_length > 0:
            return message
        res_list = message.split('\n')
        if len(res_list) <= self.context_length * 2:
            return message
        else:
            return (
                '\n'.join(res_list[:self.context_length] + ['...'] + res_list[-self.context_length:])
            )


def hide_text(text, hide):
    """Hide text for logging/output if 'hide' flag is True, return as is if flag is False

    :type text: str | unicode
    :type hide: bool
    :rtype: str | unicode
    """
    if hide:
        return u'***hidden***'
    else:
        return text
