"""
Data model over plesk_backup_xml.PleskBackup* classes to work with different versions of Plesk backup files
with an object interface. Examples of objects: Subscription, Subdomain, DnsZone.
Each object usually contains:
- several attributes 
	- some of them are read-only
	- others could be changed, and the changes are put into backup file
- methods to change the object in backup file, for example remove some information
"""

from collections import namedtuple
import base64
import itertools
from parallels.utils import parse_bool, if_not_none, obj, group_by_id
from parallels.utils.xml import elem, text_elem
from parallels.utils.ip import is_ipv4, is_ipv6, collapse_ipv4_mask


def parse_subscription_ips(ip_nodes):
	return parse_plesk_ips(ip_nodes)


def parse_plesk_ips(ip_nodes):
	ipv4 = []
	ipv6 = []
	for ip_node in ip_nodes:
		ip_address = ip_node.findtext('ip-address')
		ip_type = ip_node.findtext('ip-type')
		if is_ipv4(ip_address):
			ipv4.append(obj(ip=ip_address, ip_type=ip_type))
		elif is_ipv6(ip_address):
			ipv6.append(obj(ip=ip_address, ip_type=ip_type))
		else:
			raise Exception(u"Unknown IP address type: %s" % ip_address)

	assert 0 <= len(ipv4) <= 1
	assert 0 <= len(ipv6) <= 1

	return Ips(
		v4=ipv4[0].ip if len(ipv4) != 0 else None,
		v4_type=ipv4[0].ip_type if len(ipv4) != 0 else None,
		v6=ipv6[0].ip if len(ipv6) != 0 else None,
		v6_type=ipv6[0].ip_type if len(ipv6) != 0 else None,
	)


def get_hosting_type(site_node):
	# Plesk 10 and up have separate node for hosting
	for t in ['phosting', 'shosting', 'fhosting']:
		if site_node.find(t) is not None:
			return t
	# Plesk 9 for Windows specifies subdomain hosting as its attribute. Values there can be: vrt_hst, sub_hst
	# When such attribute is not found, return 'none'
	# FIXME won't it be better to return None instead?
	return site_node.attrib.get('hosting-type', 'none')


def is_virtual_hosting(site_node):
	return get_hosting_type(site_node) in ('phosting', 'vrt_hst', 'sub_hst')


def get_maintenance_mode(site_info):
	h_type = get_hosting_type(site_info)
	if h_type != 'none':
		p_hosting = site_info.find(h_type)
		if p_hosting is not None:
			mode = p_hosting.attrib.get('maintenance-mode', False)
			if mode:
				return mode == 'true'
	return False


def parse_logrotation_node(node):
	"""
	:rtype: parallels.common.plesk_backup.data_model.LogRotation
	"""
	if node is not None:
		return LogRotation(
			enabled=node.attrib['enabled'] == 'true',
			compress=node.attrib['compress'] == 'true',
			max_logfiles_number=int(node.attrib['max-number-of-logfiles']),
			max_size=if_not_none(node.findtext('logrotation-maxsize'), int),
			period=if_not_none(node.find('logrotation-period'), lambda n: n.attrib['period']),
			email=node.attrib.get('email'),
		)
	else:
		# default log rotation params
		return LogRotation(
			enabled=False, compress=False, max_logfiles_number=0,
			# XXX why should backup reader ever bother about the features not supported by target panel?
			# Log rotation can not be disabled in CBM, and either max_size or period must be provided. 
			# Max size must be greater than 0 (otherwise provisioning task fails), use 10240KB that is a default value in CBM.
			max_size=10240 * 1024,
			period=None, email=None
		)


def parse_php_settings_node(node):
	"""
	:rtype: dict[basestring, basestring]
	"""
	if node is not None:
		return dict((item.findtext('name'), item.findtext('value')) for item in node.findall('setting'))
	else:
		return dict() 


def read_password(password_node):
	if password_node.attrib['type'] == 'plain':
		if (
			'encoding' in password_node.attrib and
			password_node.attrib['encoding'] == 'base64' and
			password_node.text is not None
		):
			return Password('plain', base64.b64decode(password_node.text))
		else:
			return Password('plain', password_node.text or '')
	else:
		return Password(password_node.attrib['type'], password_node.text)


def parse_ip(node):
	return IpAddress(
		address=node.findtext('ip-address'),
		type=node.findtext('ip-type'),
	)


def parse_system_ip(node, is_windows):
	ip = parse_ip(node.find('ip'))

	mask_str = node.findtext('ip-netmask')
	if is_windows and is_ipv4(ip.address):
		# Windows Plesk stores IPv4 network mask in expanded form.
		mask = collapse_ipv4_mask(mask_str)
	else:
		mask = int(mask_str)

	return SystemIpAddress(
		address=ip.address,
		mask=mask,
		interface=node.findtext('ip-interface'),
		type=ip.type,
	)

Password = namedtuple('Password', ('type', 'text'))
Reseller = namedtuple('Reseller', (
	'login', 'password', 'plan_id', 'limits', 'permissions', 'contact', 'subscriptions',
	'clients', 'personal_info', 'auxiliary_user_roles', 'auxiliary_users', 'is_enabled'
))
Client = namedtuple('Client', (
	'login', 'password', 'contact', 'subscriptions', 'personal_info',
	'auxiliary_user_roles', 'auxiliary_users', 'is_enabled'
))
AuxiliaryUser = namedtuple('AuxiliaryUser', (
	'name', 'contact', 'email', 'password', 'roles',
	'personal_info', 'subscription_name', 'is_active', 'is_domain_admin', 'is_built_in'
))
AuxiliaryUserRole = namedtuple('AuxiliaryUserRole', ('name', 'permissions', 'is_domain_admin'))


class Sysuser(object):
	def __init__(self, node):
		self.node = node

	@property
	def name(self):
		return self.node.attrib['name']

	@name.setter
	def name(self, new_value):
		self.node.attrib['name'] = new_value

	@property
	def password(self):
		return if_not_none(self.node.find('password'), lambda p: read_password(p))

	def set_password(self, password, password_type):
		password_node = self.node.find('password')
		if password_node is not None:
			self.node.remove(password_node)
		new_password_node = text_elem('password', password, {'type': password_type})
		self.node.insert(0, new_password_node)

	@property
	def has_shell_access(self):
		shell = self.node.attrib.get('shell')
		return shell not in (
			None, '', 
			'Login Disabled',  # Windows-specific
			'/bin/false'  # Unix-specific
		)


class FTPUser(object):
	def __init__(self, node):
		self.node = node

	@property
	def name(self):
		return self.node.attrib['name']

	@name.setter
	def name(self, new_value):
		self.node.attrib['name'] = new_value


class ApsApplication(namedtuple('ApsApplication', ('node', 'name', 'version', 'release', 'params'))):
	def get_public_urls(self):
		"""Get public (available to end-user) application's URLs

		Returns list, each item is a tuple (
			protocol='http'|'https', 
			relative_url, relative to site's root, for example 'forum/phpBB' 
		)
		"""
		public_urls = []

		# Old APS, Plesk 9.x
		params = dict(self.params)
		application_url = params.get('application_url')
		application_protocol = params.get('p:ssl_prefix')
		if application_protocol is not None and application_url is not None:
			public_urls.append((application_protocol, application_url))

		# New APS, Plesk 11.x
		install_dir_node = self.node.find('sapp-installdir')
		if install_dir_node is not None:
			if install_dir_node.find('sapp-ssl') is not None:
				protocol = 'https'
			else:
				protocol = 'http'

			prefix = install_dir_node.findtext('sapp-prefix')

			if prefix is not None:
				public_urls.append((
					protocol, prefix
				))

		return list(set(public_urls))


class Subscription(namedtuple('Subscription', (
	'node', 'backup', 'plan_id', 'name', 'ip', 'ip_type', 'ipv6', 'ipv6_type',
	'limits', 'permissions', 'is_enabled', 'hosting_type', 'is_maintenance'
))):
	@property
	def all_databases(self):
		"""
		:rtype: list[parallels.common.plesk_backup.data_model.Database]
		"""
		return _list_subscription_databases(self.backup, self)

	def remove_database(self, database):
		"""
		:type database: parallels.common.plesk_backup.data_model.Database
		:rtype: None
		"""
		parent_map = {c: p for p in self.node.iter() for c in p}
		parent_map[database.node].remove(database.node)

	@property
	def mail_service_ips(self):
		return parse_subscription_ips(self.node.findall('mailsystem/properties/ip'))

	@property
	def dns_zone(self):
		return if_not_none(self.node.find('properties/dns-zone'), lambda dz: DnsZone.parse(self.name, dz))

	@property
	def mailsystem(self):
		return if_not_none(self.node.find('mailsystem'), lambda n: Mailsystem(n))

	@property
	def maillists(self):
		return if_not_none(self.node.find('maillists'), lambda n: Maillists(n))

	@property
	def plan(self):
		return self.backup.get_plan_by_guid(self.plan_id)

	@property
	def addon_plan_ids(self):
		ids = set()
		for plan_node in self.node.findall('preferences/subscription/plan'):
			if plan_node.attrib.get('is-addon', 'false') == 'true':
				ids.add(plan_node.attrib['plan-guid'])
		return ids

	@property
	def is_in_maintenance_mode(self):
		disabled_by_node = self.node.find('properties/status/disabled-by')
		if disabled_by_node is None:
			return False
		else:
			return disabled_by_node.attrib['name'] == 'client'

	@property
	def locked(self):
		subscription_node = self.node.find('preferences/subscription')
		if subscription_node is not None:
			return subscription_node.attrib.get('locked') == 'true'
		else:
			return False

	def find_forward_url(self):
		return self.node.findtext('fhosting') or self.node.findtext('shosting')

	@property
	def dedicated_iis_application_pool_enabled(self):
		app_pool_node = self.node.find('phosting/preferences/iis-application-pool')
		if app_pool_node is not None:
			return parse_bool(app_pool_node.attrib['turned-on'])
		else:
			return False

	def remove_plans_reference(self):
		# We remove entire 'subscription' node, as if you remove 'plan' nodes only,
		# you will get "Element 'subscription': Missing child element(s). Expected is ( plan )." error.
		# According to plesk.xsd, 'preferences/subscription' node could only contain plans
		# related to a subscription, and sync mode, so it is safe to remove it.
		prefs_node = self.node.find('preferences')
		if prefs_node is not None:
			subscription_node = prefs_node.find('subscription')
			if (
				subscription_node is not None
				# Since Plesk 12.1 we add custom attribute to identify subscription which have no plans.
				# If custom == true subscription does not contain any plans and only external_id will be restored
				and subscription_node.attrib.get('custom') != 'true'
			):
				prefs_node.remove(subscription_node)

	def remove_limit_and_permissions(self):
		limits_and_permissions = self.node.find('limits-and-permissions')
		if limits_and_permissions is not None:
			self.node.remove(limits_and_permissions)
			return True
		return False

	def remove_maillists(self):
		maillists_root = self.node.find('maillists')
		if maillists_root is not None:
			has_maillists = maillists_root.find('maillist') is not None
			self.node.remove(maillists_root)
			return has_maillists

		return False

	def has_maillists(self):
		maillists_root = self.node.find('maillists')
		if maillists_root is not None:
			return maillists_root.find('maillist') is not None
		else:
			return False

	def remove_mailsystems(self):
		domain_nodes = [self.node] + self.node.findall('phosting/sites/site')
		for domain_node in domain_nodes:
			if domain_node is not None:
				mailsystem_node = domain_node.find('mailsystem')
				if mailsystem_node is not None:
					domain_node.remove(mailsystem_node)

	def remove_default_db_servers(self):
		preferences_node = self.node.find('preferences')
		if preferences_node is not None:
			default_db_servers_node = preferences_node.find('default-db-servers')
			if default_db_servers_node is not None:
				preferences_node.remove(default_db_servers_node)

	def delete_external_id(self):
		if 'external-id' in self.node.attrib:
			del self.node.attrib['external-id']

	def get_databases(self):
		db_nodes = self.node.findall('databases/database')
		for db_node in db_nodes:
			yield Database.parse(db_node)

	def get_database_users(self, db_name, db_type):
		db_user_nodes = self.node.findall("databases/database[@name='%s'][@type='%s']/dbuser" % (db_name, db_type))
		for db_user_node in db_user_nodes:
			yield DatabaseUser.parse(db_user_node)

	def get_overall_database_users(self, db_type=None):
		"""Get database users that have access to all databases"""
		db_user_nodes = self.node.findall("databases/dbusers/dbuser")
		for db_user_node in db_user_nodes:
			user = DatabaseUser.parse(db_user_node)
			if db_type is not None and user.dbtype != db_type:
				continue
			yield user

	def get_aps_databases(self):
		db_nodes = self.node.findall('phosting/applications/sapp-installed/database')
		for db_node in db_nodes:
			yield Database.parse(db_node)

	def iter_system_users(self):
		for sysuser_node in (
			self.node.findall('phosting/preferences/sysuser')
			+ self.node.findall('phosting/webusers/webuser/sysuser')
			+ self.node.findall('phosting/ftpusers/ftpuser/sysuser')
		):
			yield if_not_none(sysuser_node, lambda n: Sysuser(n))

	def iter_additional_system_users(self):
		for sysuser_node in (
			self.node.findall('phosting/webusers/webuser/sysuser')
			+ self.node.findall('phosting/ftpusers/ftpuser/sysuser')
		):
			yield if_not_none(sysuser_node, lambda n: Sysuser(n))

	def iter_ftp_users(self):
		for ftpuser_node in (
			self.node.findall('phosting/ftpusers/ftpuser')
		):
			yield if_not_none(ftpuser_node, lambda n: FTPUser(n))

	def get_phosting_sysuser(self):
		return if_not_none(self.node.find('phosting/preferences/sysuser'), lambda n: Sysuser(n))

	def get_phosting_sysuser_name(self):
		return if_not_none(self.node.find('phosting/preferences/sysuser'), lambda n: n.attrib['name'])

	def get_phosting_webuser_names(self):
		return [
			wuser_node.attrib['name']
			for wuser_node in self.node.findall('phosting/webusers/webuser/sysuser')
		]

	def set_phosting_sysuser_name(self, name):
		self.node.find('phosting/preferences/sysuser').attrib['name'] = name

	phosting_sysuser_name = property(get_phosting_sysuser_name, set_phosting_sysuser_name)

	def change_web_ips(self, new_ips):
		"""
		:type new_ips: list[(basestring, basestring)]
		:rtype: None
		"""
		# Change IP addresses for these nodes:
		change_ips_nodes = (
			(self.node.findall('properties'), None),
			(self.node.findall('phosting/properties'), None),
			(self.node.findall('tomcat/properties'), 'status'),
			(self.node.findall('shosting/properties'), None),
			(self.node.findall('fhosting/properties'), None),

		)
		# Remove all occurences of IP address nodes from these nodes:
		remove_ips_nodes = (
			self.node.findall('phosting/sites/site/properties'),
			self.node.findall('phosting/sites/site/phosting/properties'),
			self.node.findall('phosting/sites/site/shosting/properties'),
			self.node.findall('phosting/sites/site/fhosting/properties')
		)

		for nodes, insert_after in change_ips_nodes:
			for node in nodes:
				if node is not None:
					_change_ips(node, new_ips, add_node_if_not_exists=True, insert_after=insert_after)

		for nodes in remove_ips_nodes:
			for node in nodes:
				if node is not None:
					_change_ips(node, [], add_node_if_not_exists=False)

	@property
	def has_aps(self):
		return len(self._get_sapp_installed_nodes()) > 0

	def _get_sapp_installed_nodes(self):
		return (
			self.node.findall('phosting/applications/sapp-installed')
			# In Plesk backup of versions before 10.4, subdomains applications are found here:
			+ self.node.findall('phosting/subdomains/subdomain/applications/sapp-installed')
			# In Plesk backup since version 10.4, subdomains are moved here:
			+ self.node.findall('phosting/sites/site/phosting/applications/sapp-installed')
		)

	def replace_applications(self, new_applications):
		phosting_node = self.node.find('phosting')
		if phosting_node is not None:
			if len(new_applications) > 0:
				_replace_node(
					phosting_node,
					['mime-types', 'webusers', 'ftpusers', 'frontpageusers', 'subdomains', 'sites'],
					elem('applications', new_applications)
				)
			else:
				_remove_node(phosting_node, 'applications')

	def get_aps_applications(self):
		return _get_aps_applications(self._get_sapp_installed_nodes())

	def get_direct_aps_applications(self):
		"""Get APS applications of subscription, but not of its sites"""
		return _get_aps_applications(
			self.node.findall('phosting/applications/sapp-installed'),
		)

	def iter_limits(self):
		for node in self.node.findall('limits-and-permissions/limit'):
			yield Limit.parse(node)

	@property
	def www_alias_enabled(self):
		return self.node.attrib.get('www') == 'true'

	@property
	def sitebuilder_site_id(self):
		return if_not_none(self.node.find('phosting'), lambda p: p.attrib.get('sitebuilder-site-id'))

	@property
	def https_enabled(self):
		return if_not_none(self.node.find('phosting'), lambda p: p.attrib.get('https') == 'true')

	def iter_mailboxes(self):
		mailuser_nodes = self.node.findall('mailsystem/mailusers/mailuser')
		for mailuser_node in mailuser_nodes:
			yield Mailbox.parse(mailuser_node, self.name)

	@property
	def web_ips(self):
		return Ips(v4=self.ip, v4_type=self.ip_type, v6=self.ipv6, v6_type=self.ipv6_type)

	@property
	def anonftp_enabled(self):
		"""Check whether anonymous FTP is enabled or not for subscription"""
		return self.node.find('phosting/preferences/anonftp') is not None

	def get_domain(self, name):
		for domain in self.iter_domains():
			if domain.name == name:
				return domain
		raise Exception("Domain with name '%s' not found" % name)

	def iter_domains(self):
		return itertools.chain(
			[self], self.backup.iter_sites(self.name)
		)

	def iter_subdomains(self, parent_domain_name=None):
		if parent_domain_name is None:
			parent_domain_name = self.name
		return self.backup.iter_subdomains(self.name, parent_domain_name)

	def iter_addon_domains(self):
		return self.backup.iter_addon_domains(self.name)

	def iter_sites(self):
		return self.backup.iter_sites(self.name)

	def iter_aliases(self):
		return self.backup.iter_aliases(self.name)

	@property
	def owner_login(self):
		"""Search for subscription's owner in backup."""
		client = self.backup.get_subscription_owner_client(self.name)
		return client.login

	@property
	def mime_types(self):
		"""MIME types of subscription
		
		Dictionary {extension: mime type}
		"""
		return _read_mimetypes(self.node)

	@property
	def is_virtual_hosting(self):
		return is_virtual_hosting(self.node)

	@property
	def scripting(self):
		"""
		:rtype: Scripting
		"""
		return _get_scripting(self.node)


class Scripting(object):
	def __init__(self, node):
		self._node = node

	@property
	def options(self):
		"""
		:rtype: list[parallels.common.plesk_backup.data_model.ScriptingOption]
		"""
		return [
			ScriptingOption(self._node, option_name)
			for option_name in self._node.attrib.keys()
		]


class ScriptingOption(object):
	def __init__(self, scripting_node, name):
		self._scripting_node = scripting_node
		self._name = name

	@property
	def name(self):
		"""
		:rtype: basestring
		"""
		return self._name

	@property
	def value(self):
		"""
		:rtype: basestring
		"""
		return self._scripting_node.attrib.get(self._name)

	@value.setter
	def value(self, new_value):
		self._scripting_node.attrib[self._name] = new_value


class Subscription8(namedtuple('Subscription8', (
	'node', 'backup', 'plan_id', 'name', 'ip', 'ip_type', 'ipv6', 'ipv6_type',
	'limits', 'permissions', 'is_enabled', 'hosting_type', 'is_maintenance'
))):
	@property
	def all_databases(self):
		return _list_subscription_databases(self.backup, self) 

	@property
	def mail_service_ips(self):
		# Mail IP is the same as domain one.
		return parse_subscription_ips(self.node.findall('ip'))

	@property
	def dns_zone(self):
		return if_not_none(self.node.find('dns-zone'), lambda dz: DnsZone.parse(self.name, dz))
	
	@property
	def mailsystem(self):
		return if_not_none(self.node.find('mailsystem'), lambda n: Mailsystem8(n))

	@property
	def maillists(self):
		return if_not_none(self.node.find('maillists'), lambda n: Maillists(n))

	@property
	def addon_plan_ids(self):
		return set()

	@property
	def is_in_maintenance_mode(self):
		disabled_by_node = self.node.find('status/disabled-by')
		if disabled_by_node is None:
			return False
		else:
			return disabled_by_node.attrib['name'] == 'client'

	@property
	def locked(self):
		return True

	def find_forward_url(self):
		return self.node.findtext('fhosting') or self.node.findtext('shosting')

	@property
	def dedicated_iis_application_pool_enabled(self):
		return False

	def remove_plans_reference(self):
		pass
	
	def remove_limit_and_permissions(self):
		limits = self.node.findall('limit')
		for limit in limits:
			self.node.remove(limit)

	def remove_maillists(self):
		maillists_root = self.node.find('maillists')
		if maillists_root is not None:
			has_maillists = maillists_root.find('maillist') is not None
			self.node.remove(maillists_root)
			return has_maillists

		return False

	def has_maillists(self):
		maillists_root = self.node.find('maillists')
		if maillists_root is not None:
			return maillists_root.find('maillist') is not None
		else:
			return False

	def remove_mailsystems(self):
		domain_nodes = [self.node] + self.node.findall('phosting/sites/site')
		for domain_node in domain_nodes:
			if domain_node is not None:
				mailsystem_node = domain_node.find('mailsystem')
				if mailsystem_node is not None:
					domain_node.remove(mailsystem_node)

	def remove_default_db_servers(self):
		pass

	def delete_external_id(self):
		pass

	def get_databases(self):
		db_nodes = self.node.findall('database')
		for db_node in db_nodes:
			yield Database.parse(db_node)

	def get_database_users(self, db_name, db_type):
		db_user_nodes = self.node.findall("database[@name='%s'][@type='%s']/dbuser" % (db_name, db_type))
		for db_user_node in db_user_nodes:
			yield DatabaseUser.parse(db_user_node)

	@staticmethod
	def get_overall_database_users(db_type=None):
		# Database users that have access to multiple databases were introduced in Plesk 11.1, 
		# so return empty list for old Plesk versions
		return []

	def get_aps_databases(self):
		for db_node in self.node.findall('phosting/sapp-installed/database'):
			yield Database.parse(db_node)

	def get_phosting_sysuser(self):
		return if_not_none(self.node.find('phosting/sysuser'), lambda n: Sysuser(n))

	def iter_system_users(self):
		for sysuser_node in (
			self.node.findall('phosting/sysuser')
			+ self.node.findall('phosting/webuser/sysuser')
		):
			yield if_not_none(sysuser_node, lambda n: Sysuser(n))

	def iter_additional_system_users(self):
		for sysuser_node in (
			self.node.findall('phosting/webusers/webuser/sysuser')
		):
			yield if_not_none(sysuser_node, lambda n: Sysuser(n))

	def get_phosting_sysuser_name(self):
		return if_not_none(self.node.find('phosting/sysuser'), lambda n: n.attrib['name'])

	def set_phosting_sysuser_name(self, name):
		self.node.find('phosting/sysuser').attrib['name'] = name

	phosting_sysuser_name = property(get_phosting_sysuser_name, set_phosting_sysuser_name)

	def change_web_ips(self, new_ips):
		_change_ips(
			self.node, new_ips, 
			# When domain has no hosting on PfU 8, it may have no IP address node.
			# But in certain cases (don't know exactly when - need to investigate)
			# this makes Plesk restore to fail due to Plesk bug 130416:
			# it picks one of source server's IPs,
			# with which subscription can't be restored.
			add_node_if_not_exists=True
		)

	@property
	def has_aps(self):
		return len(self._get_sapp_installed_nodes()) > 0

	def iter_limits(self):
		for node in self.node.findall('limit'):
			yield Limit.parse(node)

	@property
	def www_alias_enabled(self):
		return self.node.attrib.get('www') == 'true'

	@property
	def sitebuilder_site_id(self):
		return None  # migration of WPB sites from Plesk 8 is not supported

	def get_aps_applications(self):
		return _get_aps_applications(self._get_sapp_installed_nodes())

	def get_direct_aps_applications(self):
		"""Get APS applications of subscription, but not of its subdomains"""
		return _get_aps_applications(
			self.node.findall('phosting/sapp-installed'),
		)

	def _get_sapp_installed_nodes(self):
		return self.node.findall('phosting/sapp-installed') + self.node.findall('phosting/subdomain/sapp-installed')

	@property
	def https_enabled(self):
		return if_not_none(self.node.find('phosting'), lambda p: p.attrib.get('https') == 'true')

	def iter_mailboxes(self):
		mailuser_nodes = self.node.findall('mailsystem/mailuser')
		for mailuser_node in mailuser_nodes:
			yield Mailbox8.parse(mailuser_node, self.name)

	@property
	def web_ips(self):
		return Ips(v4=self.ip, v4_type=self.ip_type, v6=self.ipv6, v6_type=self.ipv6_type)

	def get_domain(self, name):
		for domain in self.iter_domains():
			if domain.name == name:
				return domain
		raise Exception("Domain with name '%s' not found" % name)

	def iter_domains(self):
		return itertools.chain(
			[self], self.backup.iter_sites(self.name)
		)

	def iter_sites(self):
		return self.backup.iter_sites(self.name)

	def iter_aliases(self):
		return self.backup.iter_aliases(self.name)

	@property
	def is_virtual_hosting(self):
		return is_virtual_hosting(self.node)

	@property
	def scripting(self):
		"""
		:rtype: Scripting
		"""
		return _get_scripting(self.node)


def _get_aps_applications(sapp_installed_nodes):
	return [
		ApsApplication(
			node=application_node,
			name=application_node.findtext('sapp-spec/sapp-name'),
			version=application_node.findtext('sapp-spec/sapp-version'),
			release=application_node.findtext('sapp-spec/sapp-release'),
			params=_get_aps_application_params(application_node)
		)
		for application_node in sapp_installed_nodes
	]


def _get_aps_application_params(application_node):
	params = []

	def decode_node_text(node):
		if node.text is None:
			return None
		else:
			return base64.b64decode(node.text)

	for param_node in application_node.findall('sapp-param'):
		name_node = param_node.find('sapp-param-name')
		value_node = param_node.find('sapp-param-value')
		if name_node is None: 
			continue
		if value_node is None:
			continue
		if name_node.attrib.get('encoding') != 'base64':
			continue
		if value_node.attrib.get('encoding') != 'base64':
			continue

		params.append((
			decode_node_text(name_node),
			decode_node_text(value_node)
		))
	return params


class Subdomain8(object):
	def __init__(self, subscription_name, node, parent_domain):
		self.subscription_name = subscription_name
		self.node = node
		self.parent_domain = parent_domain

	@property
	def short_name(self):
		return self.node.attrib['name']

	@property
	def name(self):
		return '%s.%s' % (self.node.attrib['name'], self.subscription_name)

	@property
	def dns_zone(self):
		return None

	def remove_dns_zone(self):
		pass  # as there is no DNS zone in Plesk 8 - there is nothing to remove

	@property
	def parent_domain_name(self):
		# Plesk backup stores parent-domain-name in IDNA
		return self.subscription_name
	
	@property
	def mailsystem(self):
		return None

	@property
	def maillists(self):
		return None

	@property
	def hosting_type(self):
		return get_hosting_type(self.node)

	@property
	def is_virtual_hosting(self):
		return is_virtual_hosting(self.node)

	def find_forward_url(self):
		return self.node.findtext('fhosting') or self.node.findtext('shosting')

	def iter_aps_databases(self):
		db_nodes = self.node.findall('applications/sapp-installed/database') + self.node.findall('sapp-installed/database')
		for db_node in db_nodes:
			yield Database.parse(db_node)

	def replace_applications(self, new_applications):
		if len(new_applications) > 0:
			_replace_node(
				self.node,
				['preferences', 'mime-types', 'sb-domain'],
				elem('applications', new_applications)
			)
		else:
			_remove_node(self.node, 'applications')

	def get_direct_aps_applications(self):
		"""Get APS applications of subdomain"""
		return _get_aps_applications(
			self.node.findall('applications/sapp-installed') +
			self.node.findall('sapp-installed')
		)

	@property
	def www_alias_enabled(self):
		return False  # in Plesk 8 there were no www aliases for subdomains

	@property
	def sitebuilder_site_id(self):
		return None  # migration of WPB sites from Plesk 8 is not supported

	@staticmethod
	def iter_mailboxes():
		return []  # there are no mailboxes on subdomains

	@property
	def is_maintenance(self):
		return False

	@property
	def is_enabled(self):
		return True  # subdomains are always enabled in Plesk 8

	@property
	def https_enabled(self):
		return self.parent_domain.https_enabled

	@property
	def web_ips(self):
		return parse_plesk_ips([])  # Subdomain in Plesk 8 does not have a separate web IP

	@property
	def scripting(self):
		"""
		:rtype: Scripting
		"""
		return _get_scripting(self.node)


class Limit(object):
	def __init__(self, node):
		self.node = node

	@classmethod
	def parse(cls, node):
		return cls(node)

	@property
	def name(self):
		return self.node.attrib.get('name')

	@property
	def value(self):
		return self.node.text

	@value.setter
	def value(self, new_value):
		self.node.text = new_value


class HostingPlan(namedtuple('HostingPlan', (
	'backup', 'node', 'id', 'name', 'is_addon', 'properties', 'log_rotation', 'php_settings'
))):
	@property
	def type(self):
		"""
		:rtype: basestring
		"""
		return 'hosting'

	@property
	def aps_filter(self):
		"""
		:rtype: parallels.common.plesk_backup.data_model.ApsBundleFilter | None
		"""
		return if_not_none(self.node.find('aps-bundle/filter'), ApsBundleFilter)

	@property
	def custom_plan_items(self):
		"""
		:rtype: list[parallels.common.plesk_backup.data_model.CustomPlanItem]
		"""
		all_plan_items = self.backup.get_custom_plan_items()
		all_plan_items_by_name = group_by_id(all_plan_items, lambda i: i.name)
		current_plan_items = []
		for plan_item in self.node.findall('template-plan-item'):
			if plan_item.text == 'true':
				name = plan_item.attrib.get('name')
				if name in all_plan_items_by_name:
					current_plan_items.append(all_plan_items_by_name[name])
		return current_plan_items

	@property
	def default_db_servers(self):
		"""
		:rtype: list[parallels.common.plesk_backup.data_model.DefaultDatabaseServer]
		"""
		return [
			DefaultDatabaseServer(node)
			for node in self.node.findall('default-db-servers/db-server')
		]

	def get_default_database_server(self, server_type):
		"""
		:rtype: parallels.common.plesk_backup.data_model.DefaultDatabaseServer
		"""
		for server in self.default_db_servers:
			if server.type == server_type:
				return server
		return None

	@property
	def php_settings_node(self):
		return self.node.find('php-settings')


class ResellerPlan(namedtuple('ResellerPlan', ('node', 'id', 'name', 'properties', 'ip_pool'))):
	@property
	def type(self):
		return 'reseller'

	@property
	def aps_filter(self):
		"""
		:rtype: parallels.common.plesk_backup.data_model.ApsBundleFilter | None
		"""
		return if_not_none(self.node.find('aps-bundle/filter'), ApsBundleFilter)


class DefaultDatabaseServer(object):
	def __init__(self, node):
		self._node = node

	@property
	def type(self):
		"""
		:rtype: basestring
		"""
		return self._node.attrib.get('type')

	@property
	def host(self):
		"""
		:rtype: basestring
		"""
		return self._node.findtext('host')

	@property
	def port(self):
		"""
		:rtype: basestring
		"""
		return self._node.findtext('port')


class CustomPlanItem(object):
	"""Custom plan item of hosting plans, also known as "Additional Services"
	"""
	def __init__(self, node):
		self._node = node
		self._properties = {
			prop_node.attrib.get('name'): prop_node.text
			for prop_node in self._node.findall('properties/plan-item-property')
		}

	@property
	def name(self):
		"""
		:rtype: basestring | None
		"""
		return self._node.attrib.get('name')

	@property
	def guid(self):
		"""
		:rtype: basestring | None
		"""
		return self._node.attrib.get('guid')

	@property
	def visible(self):
		"""
		:rtype: basestring | None
		"""
		return self._node.attrib.get('visible')

	@property
	def hint(self):
		"""
		:rtype: basestring | None
		"""
		return self._properties.get('hint')

	@property
	def description(self):
		"""
		:rtype: basestring | None
		"""
		return self._properties.get('description')

	@property
	def url(self):
		"""
		:rtype: basestring | None
		"""
		return self._properties.get('url')

	@property
	def options(self):
		"""
		:rtype: int | None
		"""
		return if_not_none(self._properties.get('options'), int)


class ApsBundleFilter(object):
	def __init__(self, node):
		self._type = node.attrib.get('type', 'white')
		self._items = [
			ApsBundleFilterItem(item_node)
			for item_node in node.findall('item')
		]

	@property
	def type(self):
		"""
		:rtype: basestring
		"""
		return self._type

	@property
	def items(self):
		"""
		:rtype: list[ApsBundleFilterItem]
		"""
		return self._items


class ApsBundleFilterItem(object):
	def __init__(self, node):
		self._name = node.findtext('name')
		self._value = node.findtext('value')

	@property
	def name(self):
		"""
		:rtype: basestring
		"""
		return self._name

	@property
	def value(self):
		"""
		:rtype: basestring
		"""
		return self._value


LogRotation = namedtuple('LogRotation', ('compress', 'max_logfiles_number', 'enabled', 'max_size', 'period', 'email'))

Ips = namedtuple('Ips', ('v4', 'v4_type', 'v6', 'v6_type'))
IpAddress = namedtuple('IpAddress', ('address', 'type'))
SystemIpAddress = namedtuple('SystemIpAddress', ('address', 'mask', 'interface', 'type'))


class DatabaseServer(object):
	def __init__(self, node, host, port, login, password, dbtype, is_default):
		self.node = node
		self.host = host
		self.port = port
		self.login = login
		self.password = password
		self.dbtype = dbtype
		self.is_default = is_default

	def set_host(self, host):
		self.host = host
		self.node.find('host').text = host


class Database(object):
	def __init__(self, node, name, dbtype, host, port, login, password, password_type):
		self.node = node
		self.name = name
		self.dbtype = dbtype
		self.host = host
		self.port = port
		self.login = login
		self.password = password
		self.password_type = password_type
	
	@staticmethod
	def parse(node):
		name = node.attrib['name']
		dbtype = node.attrib['type']

		# parse database server data
		host = node.findtext('db-server/host')
		# Microsoft SQL database at localhost has port 0, at remote host - has empty port ('')

		port = 0
		port_string = node.findtext('db-server/port')
		if port_string is not None and port_string != '':
			port = int(port_string)

		dbuser_node = node.find('dbuser')
		if dbuser_node is not None:
			login = dbuser_node.attrib['name']
			password = dbuser_node.findtext('password')
			password_type = dbuser_node.find('password').attrib['type']
		else:
			login = password = password_type = None
		return Database(node, name, dbtype, host, port, login, password, password_type)

	def change_location(self, location):
		self.host = location.host
		self.port = location.port

		host_node = self.node.find('db-server/host')
		if host_node is not None:
			host_node.text = location.host

		port_node = self.node.find('db-server/port')
		if port_node is not None:
			port_node.text = str(location.port) if location.port is not None else ''

	def set_host(self, host):
		self.host = host
		self.node.find('db-server/host').text = host


class DatabaseUser(object):
	def __init__(self, node, name, password, password_type, dbtype=None, host=None, port=None):
		self.node = node
		self.name = name
		self.password = password
		self.password_type = password_type
		self.dbtype = dbtype
		self.host = host
		self.port = port

	@staticmethod
	def parse(node):
		name = node.attrib['name']
		password_node = node.find('password')
		if password_node is not None:
			password = password_node.text
			password_type = password_node.attrib['type']
		else:
			password = password_type = None
		dbtype = if_not_none(node.find('db-server'), lambda d: d.attrib.get('type'))
		host = node.findtext('db-server/host')

		port = 0
		port_string = node.findtext('db-server/port')
		if port_string is not None and port_string != '':
			port = int(port_string)

		return DatabaseUser(node, name, password, password_type, dbtype, host, port)

	def change_location(self, location):
		host_node = self.node.find('db-server/host')
		if host_node is not None:
			host_node.text = location.host
		port_node = self.node.find('db-server/port')
		if port_node is not None:
			port_node.text = str(location.port) if location.port is not None else ''


class Mailbox(object):
	def __init__(
		self, node, name, domain_name, password, password_type, enabled,
		quota
	):
		self.node = node
		self.name = name
		self.domain_name = domain_name
		self.password = password
		self.password_type = password_type
		self.enabled = enabled
		self.quota = quota
	
	@staticmethod
	def parse(node, domain_name):
		name = node.attrib['name']
		password = node.findtext('properties/password')
		password_type = if_not_none(
			node.find('properties/password'), lambda pnode: pnode.attrib.get('type')
		)
		enabled = if_not_none(
			node.find('preferences/mailbox'),
			lambda mnode: parse_bool(mnode.attrib.get('enabled'))
		)
		quota = if_not_none(node.attrib.get('mailbox-quota'), lambda q: int(q))
		return Mailbox(
			node, name, domain_name, password, password_type, enabled,
			quota
		)

	@property
	def full_name(self):
		return '%s@%s' % (self.name, self.domain_name)

	def set_password(self, password):
		properties_node = self.node.find('properties')
		if properties_node is None:
			properties_node = elem('properties')
			self.node.insert(0, properties_node)
		password_node = properties_node.find('password')
		if password_node is None:
			password_node = elem('password')
			properties_node.insert(0, password_node)

		password_node.attrib['type'] = 'plain'
		password_node.text = password

	def remove_spamassassin(self):
		self._remove_preferenses_subnode('spamassassin')

	def remove_virusfilter(self):
		self._remove_preferenses_subnode('virusfilter')

	def _remove_preferenses_subnode(self, subnode_name):
		preferences_node = self.node.find('preferences')
		if preferences_node is not None:
			subnode = preferences_node.find(subnode_name)
			if subnode is not None:
				preferences_node.remove(subnode)


class Mailbox8(object):
	def __init__(self, node, name, domain_name, password, password_type, enabled, quota):
		self.node = node
		self.name = name
		self.domain_name = domain_name
		self.password = password
		self.password_type = password_type
		self.enabled = enabled
		self.quota = quota
	
	@staticmethod
	def parse(node, domain_name):
		name = node.attrib['name']
		password = node.findtext('password')
		password_type = if_not_none(node.find('password'), lambda pnode: pnode.attrib.get('type'))

		mailbox_node = node.find('mailbox')
		if mailbox_node is not None:
			if mailbox_node.attrib.get('enabled') != 'false':
				enabled = True
			else:
				enabled = False
		else:
			enabled = False
		quota = if_not_none(node.attrib.get('mailbox-quota'), lambda q: int(q))
		return Mailbox8(node, name, domain_name, password, password_type, enabled=enabled, quota=quota)

	@property
	def full_name(self):
		return '%s@%s' % (self.name, self.domain_name)

	def set_password(self, password):
		password_node = self.node.find('password')
		if password_node is None:
			password_node = elem('password')
			self.node.insert(0, password_node)

		password_node.attrib['type'] = 'plain'
		password_node.text = password

	def remove_spamassassin(self):
		self._remove_subnode('spamassassin')

	def remove_virusfilter(self):
		self._remove_subnode('virusfilter')

	def _remove_subnode(self, subnode_name):
		subnode = self.node.find(subnode_name)
		if subnode is not None:
			self.node.remove(subnode)

DnsSettings = namedtuple('DnsSettings', ('do_subdomains_own_zones',))


class DnsZoneBase(object):
	"""Entity class for DNS zone"""

	def __init__(self, domain_name, zone_type, enabled):
		self.domain_name = domain_name
		self.zone_type = zone_type
		self.enabled = enabled

	def iter_dns_records(self):
		raise NotImplementedError()

	def add_dns_record(self, record):
		raise NotImplementedError()

	def remove_dns_record(self, record):
		raise NotImplementedError()

	def enable(self):
		raise NotImplementedError()


class DnsZone(DnsZoneBase):
	"""DNS zone based on backup XML node"""

	def __init__(self, domain_name, node, zone_type, enabled):
		self._node = node
		super(DnsZone, self).__init__(domain_name, zone_type, enabled)

	@staticmethod
	def parse(domain_name, node):
		return DnsZone(
			domain_name,
			node,
			zone_type=node.attrib['type'],
			enabled=node.find('status/enabled') is not None
		)

	def iter_dns_records(self):
		for n in self._node.iterfind('dnsrec'):
			# XXX Workaround for bug #106623: skip "slave zone" marker record which doesn't
			# have 'src' and 'dst' attributes and causes parsing failure.
			if n.attrib['type'] == 'master':
				continue
			yield DnsRecord.parse(n)

	def remove_marker_records(self):
		nodes_to_remove = []
		for n in self._node.iterfind('dnsrec'):
			if n.attrib['type'] == 'master':
				nodes_to_remove.append(n)

		for n in nodes_to_remove:
			self._node.remove(n)

	def add_dns_record(self, record):
		record_xml_node = elem('dnsrec', [], {
			'type': record.rec_type, 'src': record.src, 'dst': record.dst, 'opt': record.opt
		})
		self._node.append(record_xml_node)
		return DnsRecord(node=record_xml_node, src=record.src, dst=record.dst, opt=record.opt, rec_type=record.rec_type)

	def remove_dns_record(self, record):
		self._node.remove(record._node)

	def enable(self):
		for index, node in enumerate(self._node):
			if node.tag == 'status':
				self._node[index] = elem('status', [elem('enabled')])
				return


class DnsRecordBase(object):
	"""Entity class for DNS record"""

	def __init__(self, src, dst, opt, rec_type):
		self.src = src
		self.dst = dst
		self.opt = opt
		self.rec_type = rec_type

	def change(self, src, dst):
		self.src = src
		self.dst = dst

	def __hash__(self):
		return hash(self.rec_type) ^ hash(self.src) ^ hash(self.dst) ^ hash(self.opt)

	def __eq__(self, other):
		return (
			self.rec_type == other.rec_type and 
			self.src == other.src and self.dst == other.dst and self.opt == other.opt
		)

	def __repr__(self):
		return "DnsRecord(rec_type={rec_type} src='{src}' dst='{dst}' opt='{opt}')".format(**self.__dict__)


class DnsRecord(DnsRecordBase):
	"""DNS record based on backup XML node"""

	def __init__(self, node, src, dst, opt, rec_type):
		self._node = node
		super(DnsRecord, self).__init__(src, dst, opt, rec_type)
	
	@classmethod 
	def parse(cls, node):
		src = node.attrib['src']
		dst = node.attrib.get('dst', '')
		opt = node.attrib.get('opt', '')
		rec_type = node.attrib['type']

		if rec_type in ('A', 'AAAA', 'MX', 'TXT', 'CNAME', 'NS'):
			src = src.encode('idna')
		if rec_type in ('MX', 'CNAME', 'NS', 'PTR'):
			dst = dst.encode('idna')
		return DnsRecord(node, src, dst, opt, rec_type)

	def change(self, src, dst):
		self._node.attrib['src'] = src
		self._node.attrib['dst'] = dst
		super(DnsRecord, self).change(src, dst)


class AddonDomain(object):
	def __init__(self, node):
		self.node = node

	@property
	def name(self):
		return self.node.attrib['name']
	
	@property
	def is_enabled(self):
		return self.node.find('properties/status/enabled') is not None

	@property
	def is_maintenance(self):
		return get_maintenance_mode(self.node)

	@property
	def dns_zone(self):
		return if_not_none(self.node.find('properties/dns-zone'), lambda dz: DnsZone.parse(self.name, dz))

	def remove_dns_zone(self):
		properties_node = self.node.find('properties')
		if properties_node is not None:
			zone_node = properties_node.find('dns-zone')
			if zone_node is not None:
				properties_node.remove(zone_node)

	@property
	def parent_domain_name(self):
		# Plesk backup stores parent-domain-name in IDNA
		return if_not_none(self.node.attrib.get('parent-domain-name'), lambda s: s.decode("idna"))
	
	@property
	def mailsystem(self):
		return if_not_none(self.node.find('mailsystem'), lambda n: Mailsystem(n))

	@property
	def maillists(self):
		return if_not_none(self.node.find('maillists'), lambda n: Maillists(n))

	@property
	def hosting_type(self):
		return get_hosting_type(self.node)

	@property
	def is_virtual_hosting(self):
		return is_virtual_hosting(self.node)

	def find_forward_url(self):
		return self.node.findtext('fhosting') or self.node.findtext('shosting')

	def iter_aps_databases(self):
		db_nodes = self.node.findall('phosting/applications/sapp-installed/database')
		for db_node in db_nodes:
			yield Database.parse(db_node)

	def replace_applications(self, new_applications):
		phosting_node = self.node.find('phosting')
		if phosting_node is not None:
			if len(new_applications) > 0:
				_replace_node(
					phosting_node,
					['mime-types', 'webusers', 'ftpusers', 'frontpageusers', 'subdomains', 'sites'],
					elem('applications', new_applications)
				)
			else:
				_remove_node(phosting_node, 'applications')

	@property
	def www_alias_enabled(self):
		return self.node.attrib.get('www') == 'true'

	@property
	def sitebuilder_site_id(self):
		return if_not_none(self.node.find('phosting'), lambda p: p.attrib.get('sitebuilder-site-id'))

	@property
	def https_enabled(self):
		return if_not_none(self.node.find('phosting'), lambda p: p.attrib.get('https') == 'true')

	def iter_mailboxes(self):
		mailuser_nodes = self.node.findall('mailsystem/mailusers/mailuser')
		for mailuser_node in mailuser_nodes:
			yield Mailbox.parse(mailuser_node, self.name)

	@property
	def web_ips(self):
		"""Return site's web IP addresses, if any are specified.
		It is technically possible to specify a separate IP address for a site since Plesk 10.
		Plesk is not using that, but H-Sphere is using.
		"""
		return parse_plesk_ips(self.node.findall('properties/ip'))

	def remove_mailsystem(self):
		mailsystem_node = self.node.find('mailsystem')
		if mailsystem_node is not None:
			self.node.remove(mailsystem_node)

	def get_direct_aps_applications(self):
		"""Get APS applications of addon domain"""
		return _get_aps_applications(
			self.node.findall('phosting/applications/sapp-installed'),
		)

	@property
	def mime_types(self):
		"""MIME types of domain
		
		Dictionary {extension: mime type}
		"""
		return _read_mimetypes(self.node)

	@property
	def scripting(self):
		"""
		:rtype: Scripting
		"""
		return _get_scripting(self.node)


class Subdomain(AddonDomain):
	@property
	def short_name(self):
		return self.name[0:-1*len(self.parent_domain_name)-1]


class DomainAlias(object):
	def __init__(self, parent_domain_name, node):
		self.parent_domain_name = parent_domain_name
		self.node = node

	@property
	def name(self):
		return self.node.attrib['name']

	@property
	def mail(self):
		return self.node.attrib['mail']

	def enable_mail(self):
		self.node.attrib['mail'] = 'true'

	@property
	def dns_zone(self):
		return if_not_none(self.node.find('dns-zone'), lambda dz: DnsZone.parse(self.name, dz))

	@property
	def mailsystem(self):
		return None

	@property
	def maillists(self):
		return None


class MailsystemBase(object):
	def __init__(self, node):
		self.node = node

	def replace_catch_all(self, value):
		"""Replace catch all value with a new one. However if catch-all node does not exist, nothing will be done"""
		preferences_node = self.node.find('preferences')
		if preferences_node is not None:
			catch_all_node = preferences_node.find('catch-all')
			if catch_all_node is not None:
				catch_all_node.text = value

	def get_catch_all(self):
		return if_not_none(self.node.find('preferences'), lambda n: n.findtext('catch-all'))


class Mailsystem(MailsystemBase):
	def change_ips(self, new_ips):
		properties_node = self.node.find('properties')
		_change_ips(properties_node, new_ips)

	@property
	def ips(self):
		return parse_plesk_ips(self.node.findall('properties/ip'))

	def enable(self):
		properties_node = self.node.find('properties')
		for index, node in enumerate(properties_node):
			if node.tag == 'status':
				properties_node[index] = elem('status', [elem('enabled')])
				return

	@property
	def enabled(self):
		return self.node.find('properties/status/enabled') is not None
	
	def set_webmail(self, webmail):
		webmail_node = self.node.find('preferences/web-mail')
		if webmail_node is not None:
			webmail_node.text = webmail
	
	def get_webmail(self):
		webmail_node = self.node.find('preferences/web-mail')
		if webmail_node is not None:
			return webmail_node.text
		return None

	def add_domainkeys(self, dkey_public, dkey_private):
		dkeys_node = self.node.find('preferences/domain-keys')
		if dkeys_node is not None:
			dkeys_node.attrib['public-key'] = dkey_public
			dkeys_node.attrib['private-key'] = base64.b64encode(dkey_private)

	def remove_domainkeys(self):
		self._remove_preferenses_subnode('domain-keys')

	def remove_greylisting(self):
		self._remove_preferenses_subnode('grey-listing')

	def _remove_preferenses_subnode(self, subnode_name):
		preferences_node = self.node.find('preferences')
		if preferences_node is not None:
			subnode = preferences_node.find(subnode_name)
			if subnode is not None:
				preferences_node.remove(subnode)


class Mailsystem8(MailsystemBase):
	def change_ips(self, new_ips):
		pass  # no changes are required for Plesk 8.x backup, as it does not contain any mail IP addresses

	def set_webmail(self, webmail):
		webmail_node = self.node.find('preferences/web-mail')
		if webmail_node is not None:
			webmail_node.text = webmail
	
	def get_webmail(self):
		webmail_node = self.node.find('preferences/web-mail')
		if webmail_node is not None:
			return webmail_node.text
		return None

	def enable(self):
		for index, node in enumerate(self.node):
			if node.tag == 'status':
				self.node[index] = elem('status', [elem('enabled')])
				return

	@property
	def enabled(self):
		return self.node.find('status/enabled') is not None

	def add_domainkeys(self, dkey_public, dkey_private):
		dkeys_node = self.node.find('domain-keys')
		if dkeys_node is not None:
			dkeys_node.attrib['public-key'] = dkey_public
			dkeys_node.attrib['private-key'] = base64.b64encode(dkey_private)

	def remove_domainkeys(self):
		self._remove_subnode('domain-keys')

	def remove_greylisting(self):
		self._remove_subnode('grey-listing')

	def _remove_subnode(self, subnode_name):
		subnode = self.node.find(subnode_name)
		if subnode is not None:
			self.node.remove(subnode)


class Maillists(object):
	def __init__(self, node):
		self.node = node

	def change_ips(self, new_ips):
		properties_node = self.node.find('properties')
		if properties_node is not None:
			_change_ips(properties_node, new_ips)


def _read_mimetypes(node):
	"""Read MIME types from domain/subscription node

	Arguments:
	- node - domain or subscription node
	"""
	result = {}
	for mimetype_node in node.findall('phosting/mime-types/mime-type'):
		result[mimetype_node.attrib['ext']] = mimetype_node.attrib['value']
	return result


def _change_ips(root_node, new_ips, add_node_if_not_exists=False, insert_after=None):
	"""
	:type new_ips: list[(basestring, basestring)]
	:type add_node_if_not_exists: bool
	:type insert_after: basestring
	:rtype: None
	"""
	ip_nodes = root_node.findall('ip')
	if len(ip_nodes) == 0:
		if add_node_if_not_exists:
			insert_at_index = 0
			if insert_after is not None:
				for index, node in enumerate(root_node, start=1):
					if node.tag == insert_after:
						insert_at_index = index
						break

			for ip_address, ip_type in new_ips:
				if ip_address is not None:
					new_ip_node = elem('ip', [
						text_elem('ip-type', ip_type),
						text_elem('ip-address', ip_address),
					])
					root_node.insert(insert_at_index, new_ip_node)
		else:
			# if there were no IP nodes - we consider that backup of that particular
			# version should not contain any IP addresses in root_node
			# for example Plesk 9.x has no IP nodes in mailsystem node => we should not insert
			# these nodes with new mail service IPs as
			# Plesk restore will detect IP addresses itself
			return
	else:
		# detect position of IP nodes in root_node
		first_ip_node_index = None
		for index, node in enumerate(root_node):
			if node.tag == 'ip':
				first_ip_node_index = index
				break
		assert(first_ip_node_index is not None)

		# remove all existing IP nodes from root_node
		for ip_node in ip_nodes:
			root_node.remove(ip_node)

		# add IP nodes with new IPs
		inserted_cnt = 0
		for ip_address, ip_type in new_ips:
			if ip_address is not None:
				new_ip_node = elem('ip', [
					text_elem('ip-type', ip_type),
					text_elem('ip-address', ip_address),
				])
				root_node.insert(first_ip_node_index + inserted_cnt, new_ip_node)
				inserted_cnt += 1


def _replace_node(parent_node, next_nodes_names, new_node):
	"""Replace all nodes named as given new_node and located under parent_node with the new_node.
	@param parent_node
	@param next_nodes_names - names of all nodes located in schema under given parent_node and after new_node
	@param new_node
	"""
	_remove_node(parent_node, new_node.tag)

	new_idx = 0
	for idx, node in enumerate(parent_node):
		if node.tag in next_nodes_names:
			break
		else:
			new_idx = idx + 1

	parent_node.insert(new_idx, new_node)


def _remove_node(parent_node, node_tag):
	for old_node in parent_node.findall(node_tag):
		parent_node.remove(old_node)


def _list_subscription_databases(backup, subscription):
	addon_databases = sum([
		list(addon.iter_aps_databases()) 
		for addon in backup.iter_addon_domains(subscription.name)
	], [])
	subdomain_databases = sum([
		list(subdomain.iter_aps_databases())
		for domain in itertools.chain(
			[subscription], 
			backup.iter_addon_domains(subscription.name)
		)
		for subdomain in backup.iter_subdomains(
			subscription.name, domain.name
		)
	], [])
	return sum([
		list(subscription.get_databases()), 
		list(subscription.get_aps_databases()), 
		addon_databases, 
		subdomain_databases
	], [])


def _get_scripting(domain_node):
	"""
	:rtype: parallels.common.plesk_backup.data_model.Scripting
	"""
	node = domain_node.find('phosting/limits-and-permissions/scripting')
	if node is None:
		return None
	return Scripting(node)