from __future__ import absolute_import
import collections
import copy
import itertools
import xml.etree.ElementTree as et

XmlrpcHost = collections.namedtuple('XmlrpcHost', 'value')
XmlrpcMessage = collections.namedtuple('XmlrpcMessage', 'data')
PositionalArgCommand = collections.namedtuple(
'PositionalArgCommand', 'command, arguments, serializer')
NamedArgCommand = collections.namedtuple(
'NamedArgCommand', 'command, arguments, serializer')

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.
	"""

	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']
		self.xmlrpc_sensitive_fields = ['passwd', 'password']
		self.message_walk_maxdepth = 11
		self.dummy_text = '***hidden***'

	def info(self, message, *args):
		self.logger.info(message, *self.get_sane_args(*args))

	def debug(self, message, *args):
		self.logger.debug(message, *self.get_sane_args(*args))

	def error(self, message, *args):
		self.logger.error(message, *self.get_sane_args(*args))

	def format(self, message, *args):
		"""Clean up the message and return it as a formatted string."""
		return message % tuple(self.get_sane_args(*args))

	def get_sane_args(self, *args):
		"""Remove sensitive data from log message arguments."""
		sane_args = []
		for arg in args:
			if isinstance(arg, et.ElementTree):
				sane_args.append(self.cleanup_xmltree(arg))
			elif isinstance(arg, PositionalArgCommand):
				sane_args.append(self.cleanup_cli_command(arg))
			elif isinstance(arg, NamedArgCommand):
				sane_args.append(self.cleanup_named_arg_command(arg))
			elif isinstance(arg, XmlrpcMessage):
				sane_args.append(self.cleanup_xmlrpc_message(arg))
			else:
				sane_args.append(arg)
		return sane_args

	def cleanup_cli_command(self, command):
		"""Remove cleartext passwords from CLI arguments.
		
		Parameters:
			command: a 'PositionalArgCommand' object
		
		Returns: arguments formatted to string.
		"""
		sane_args, do_bleep = [], False
		arguments = command.arguments if command.arguments is not None else []
		for arg in arguments:
			sane_arg = arg if arg == '' or not do_bleep else self.dummy_text
			sane_args.append(sane_arg)
			# for example, if argument is '-passwd', then bleep the next arg
			do_bleep = True if arg in self.cli_sensitive_arguments else False
		return command.serializer(command.command, sane_args)

	def cleanup_named_arg_command(self, command):
		"""Remove cleartext passwords from CLI arguments.
		
		Parameters:
			command: a 'NamedArgCommand' object
		
		Returns: a command with arguments formatted to string.
		"""
		sane_args = {}
		if command.arguments is not None:
			for arg, arg_value in command.arguments.iteritems():
				if arg_value == '' or arg not in self.dict_cli_sensitive_arguments:
					sane_args[arg] = arg_value
				else:
					sane_args[arg] = self.dummy_text
		return command.serializer(command.command, sane_args)

	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
		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.iteritems():
				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 get_clean_xmlrpc_uri(self, url):
		"""Remove password from XML RPC connection string.
		
		For example, the URL
			admin:setup@10.52.38.8:8440
		- will be changed this way:
			admin:***hidden***@10.52.38.8:8440:
		"""
		cut = url.rfind('@')
		if -1 == cut:
			return url
		else:
			return self.dummy_text + url[cut:]