from parallels.plesk.source.custom import messages
import json
import random
import re
import string
import uuid
import os
import logging
from parallels.core import MigrationError
from parallels.core.utils.common_constants import DATABASE_NO_SOURCE_HOST

from parallels.core.utils.yaml_utils import read_yaml
from xml.etree import ElementTree
from parallels.core.dump import save_backup_tar
from parallels.core.utils.common import (
	xml, if_not_none, group_by_id, generate_random_password, is_empty, default, find_only,
	is_ascii_string)
from parallels.core.utils.common.xml import elem, text_elem, seq, seq_iter, xml_to_string_pretty


logger = logging.getLogger(__name__)


class HostingDescriptionToPleskDumpConverter(object):
	"""Convert hosting description file to Plesk configuration dump format"""

	def write_dump(self, hosting_description_config, target_dump_filename, database_servers=None, is_windows=False):
		"""Write Plesk configuration dump file created out of hosting description file

		:type hosting_description_config: parallels.custom_panel_migrator.connections.HostingDescriptionConfig
		:type target_dump_filename: basestring
		:type database_servers: list[parallels.custom_panel_migrator.connections.DatabaseServerConfig] | None
		"""
		database_servers = default(database_servers, [])
		data = read_hosting_description_file(hosting_description_config)
		self._write_dump_for_data(data, target_dump_filename, database_servers, is_windows)

	def _write_dump_for_data(self, data, target_dump_filename, database_servers=None, is_windows=False):
		"""
		:type data: list
		:type target_dump_filename: basestring
		:type database_servers: list[parallels.custom_panel_migrator.connections.DatabaseServerConfig] | None
		"""
		database_servers = default(database_servers, [])
		dump_tree = self._create_dump_xml_tree(data, database_servers, is_windows)

		self._save(dump_tree, target_dump_filename)

	def _create_dump_xml_tree(self, data, database_servers=None, is_windows=False):
		"""
		:type data: list
		:type database_servers: list[parallels.custom_panel_migrator.connections.DatabaseServerConfig] | None
		"""
		database_servers = default(database_servers, [])
		dump_tree = ElementTree.ElementTree(
			xml.elem(
				'migration-dump', [], {
					'content-included': 'false',
					'agent-name': 'PleskX',
					'dump-format': 'panel',
					'dump-version': '11.0.9'
				}
			)
		)
		root = dump_tree.getroot()
		if is_windows:
			root.append(xml.elem('dump-info', [
				elem('os-description', [], {'type': 'windows'})
			], {}))
		root.append(xml.elem('admin', [], {'guid': guid_str()}))
		root.append(xml.elem('server'))
		root.find('admin').append(xml.elem('clients', [
			self._client_node(client, database_servers) for client in data
		]))
		if len(database_servers) > 0:
			root.find('server').append(xml.elem('db-servers', [
				elem(
					'db-server',
					[
						text_elem('host', db_server.host),
						text_elem('port', db_server.port),
						elem(
							'db-admin',
							[
								self._password_node(db_server.password)
							],
							{
								'name': db_server.login
							}
						)
					],
					{
						'type': db_server.db_type
					}
				)
				for db_server in database_servers
			]))

		return dump_tree

	def _client_node(self, client, database_servers):
		return elem(
			'client',
			[
				self._client_preferences_node(client),
				self._client_properties_node(client),
				self._client_domains_node(client, database_servers)
			],
			{
				'guid': guid_str(),
				'name': client['login'],
				'contact': client.get('name', '')
			}
		)

	@staticmethod
	def _pinfo_node(name, value):
		if value is None:
			return None
		else:
			return text_elem('pinfo', value, {'name': name})

	@staticmethod
	def _client_preferences_node(client):
		return elem('preferences', seq(
			text_elem('pinfo', client.get('email'), {'name': 'email'})
		))

	def _client_properties_node(self, client):
		return elem('properties', [
			self._password_node(client.get('password')),
			self._enabled_status_node()
		])

	@staticmethod
	def _enabled_status_node():
		return elem('status', [elem('enabled')])

	def _client_domains_node(self, client, database_servers):
		subscriptions = client.get('subscriptions', [])
		return elem(
			'domains', [
				self._domain_node(subscription, database_servers) for subscription in subscriptions
			]
		)

	def _domain_node(self, domain, database_servers):
		return elem(
			'domain', seq(
				elem('preferences'),
				self._domain_properties_node(),
				self._mailsystem_node(domain, domain['name']),
				self._databases_node(domain, database_servers),
				self._phosting_node(domain)
			),
			{
				'guid': guid_str(),
				'www': 'true',
				'name': domain['name']
			}
		)

	def _domain_properties_node(self):
		return elem(
			'properties',
			[
				elem('ip', [
					text_elem('ip-type', 'shared'),
					text_elem('ip-address', '127.0.0.1')
				]),
				self._enabled_status_node()
			]
		)

	def _mailsystem_node(self, domain, domain_name_filter):
		return elem(
			'mailsystem', seq(
				elem('properties', [self._enabled_status_node()]),
				self._mailusers_node(domain, domain_name_filter),
				elem('preferences'),
			)
		)

	def _mailusers_node(self, domain, domain_name_filter):
		mailboxes = domain.get('mailboxes')
		if mailboxes is None:
			return None

		mailboxes_filtered = [
			mailbox for mailbox in mailboxes
			if self._mailbox_domain_name(domain, mailbox) == domain_name_filter
		]
		if len(mailboxes_filtered) == 0:
			return None

		else:
			return elem('mailusers', [
				self._mailuser_node(mailbox) for mailbox in mailboxes_filtered
			])

	@staticmethod
	def _mailbox_domain_name(domain, mailbox):
		if '@' in mailbox['name']:
			return mailbox['name'][mailbox['name'].find('@')+1:]
		else:
			return domain['name']

	def _mailuser_node(self, mailbox):
		return elem(
			'mailuser',
			seq(
				elem('properties', [self._password_node(mailbox.get('password'))]),
				elem('preferences', seq(
					elem('mailbox', [], {'enabled': 'true', 'type': 'mdir'}),
					self._spamassassin_node(mailbox),
					self._virusfilter_node(mailbox)
				))
			),
			{
				'name': self._short_mailbox_name(mailbox['name']),
				'forwarding-enabled': 'false',
				'mailbox-quota': str(self._convert_to_bytes(mailbox.get('limit')))
			}
		)

	@staticmethod
	def _convert_to_bytes(value):
		if is_empty(value):
			return -1

		value = value.strip()
		m = re.match('^(\d+)\s*(M|K|)$', value)
		if m is None:
			raise Exception(messages.INVALID_FORMAT % value)
		val = m.group(1)
		multiplier = m.group(2)

		if is_empty(multiplier):
			return int(val)
		elif multiplier == 'K':
			return int(val) * 1024
		elif multiplier == 'M':
			return int(val) * 1024 * 1024
		else:
			assert False, messages.INVALID_MULTIPLIER % multiplier

	def _spamassassin_node(self, mailbox):
		spamassassin = mailbox.get('spamfilter')
		if spamassassin is None:
			return None
		else:
			attribs = {
				'status': 'on' if self._parse_bool_value(spamassassin) else 'off'
			}
			if self._parse_bool_value(spamassassin):
				attribs['subj-text'] = '***SPAM***'
			return elem(
				'spamassassin', [], attribs
			)

	def _virusfilter_node(self, mailbox):
		antivirus = mailbox.get('antivirus')
		if antivirus is None:
			return None
		else:
			return elem(
				'virusfilter', [],
				{
					'state': 'inout' if self._parse_bool_value(antivirus) else 'none'
				}
			)

	@staticmethod
	def _parse_bool_value(value):
		if value in (1, True, '1', 'on', 'true', 'enabled'):
			return True
		elif value in (0, False, '0', 'off', 'false', 'disabled'):
			return False
		else:
			raise Exception(
				messages.INVALID_BOOLEAN_VALUE % value
			)

	@staticmethod
	def _password_node(password):
		if password is None:
			password = generate_random_password()
		return text_elem('password', password, {'type': 'plain'})

	def _phosting_node(self, domain):
		return elem(
			'phosting',
			seq(
				elem('preferences', [
					self._sysuser_node(domain.get('sys_user', {})),
				]),
				elem('limits-and-permissions', [elem('scripting')]),
				self._sites_node(domain)
			),
			{
				'www-root': domain.get('target_document_root', 'httpdocs')
			}
		)

	def _sysuser_node(self, sysuser):
		return elem(
			'sysuser',
			[
				self._password_node(sysuser.get('password'))
			],
			{
				'name': sysuser.get('login', self._random_login('sub'))
			}
		)

	@staticmethod
	def _random_login(prefix):
		random_digits = "".join(random.choice(string.digits) for _ in range(10))
		return "%s_%s" % (prefix, random_digits,)

	def _sites_node(self, domain):
		addon_domains = domain.get('addon_domains', [])
		subdomains = domain.get('subdomains', [])
		if len(addon_domains) + len(subdomains) == 0:
			return None
		else:
			return elem(
				'sites',
				(
					seq_iter(self._addon_domain_node(domain, addon_domain) for addon_domain in addon_domains) +
					seq_iter(self._subdomain_node(domain, subdomain) for subdomain in subdomains)
				)
			)

	def _addon_domain_node(self, domain, addon_domain):
		return elem(
			'site',
			[
				elem('preferences'),
				self._domain_properties_node(),
				self._mailsystem_node(domain, addon_domain['name']),
				self._site_phosting_node(addon_domain)
			],
			{
				'guid': guid_str(),
				'name': addon_domain['name']
			}
		)

	def _subdomain_node(self, domain, subdomain):
		parent_name = subdomain.get('parent-domain', self._get_parent(domain, subdomain))
		return elem(
			'site',
			[
				elem('preferences'),
				self._domain_properties_node(),
				self._site_phosting_node(subdomain)
			],
			{
				'guid': guid_str(),
				'name': subdomain['name'],
				'parent-domain-name': parent_name
			}
		)

	@staticmethod
	def _get_parent(domain, subdomain):
		all_domains = [domain['name']] + [addon['name'] for addon in domain.get('addon_domains', [])]
		for domain_name in all_domains:
			if subdomain['name'].endswith('.%s' % domain_name):
				return domain_name

	@classmethod
	def _site_phosting_node(cls, site):
		return elem(
			'phosting',
			seq(elem('preferences')),
			{'www-root': cls._get_site_document_root(site)}
		)

	@staticmethod
	def _get_site_document_root(site):
		document_root = site.get('target_document_root')
		if document_root is not None:
			if not is_ascii_string(document_root):
				raise MigrationError(
					messages.VALIDATION_NON_ASCII_DOCUMENT_ROOT.format(site=site['name'], document_root=document_root)
				)
			return document_root
		else:
			return site['name'].encode('idna')

	@staticmethod
	def _short_mailbox_name(mailbox_name):
		if '@' in mailbox_name:
			return mailbox_name[:mailbox_name.find('@')]
		else:
			return mailbox_name

	def _databases_node(self, domain, database_servers):
		databases = domain.get('databases', [])
		if len(databases) == 0:
			return None
		else:
			return elem('databases', seq_iter(
				self._database_node(database, database_servers) for database in databases
			))

	def _database_node(self, database, database_servers):
		if database.get('host') is not None and database.get('port') is not None and database.get('type') is not None:
			host = database.get('host')
			port = database.get('port')
			db_type = database.get('type')
		elif database.get('server') is not None:
			db_server_id = database.get('server')
			db_server = find_only(
				database_servers, lambda d: d.db_server_id == db_server_id,
				messages.FAILED_TO_FIND_DB_SERVER.format(
					db_server_id=db_server_id
				)
			)
			host = db_server.host
			port = db_server.port
			db_type = db_server.db_type
		else:
			# We don't have information about source database server, so we should not try
			# to copy database content in a regular way by connecting to the source database server.
			# So we mark that in backup dump by special constant 'DATABASE_NO_SOURCE_HOST'.
			host = database.get('host', DATABASE_NO_SOURCE_HOST)
			port = database.get('port', 0)
			db_type = database.get('type', 'mysql')

		return elem(
			'database',
			seq(
				elem(
					'db-server',
					seq(
						text_elem('host', host),
						text_elem('port', port),
					),
					{
						'type': db_type
					}
				),
				if_not_none(database.get('user'), self._database_user)
			),
			{
				'name': database['name'],
				'type': db_type
			}
		)

	def _database_user(self, user):
		return elem(
			'dbuser',
			seq(
				self._password_node(user.get('password'))
			),
			{
				'name': user.get('login', self._random_login('db'))
			}
		)

	@staticmethod
	def _save(dump_tree, target_dump_filename):
		if os.path.exists(target_dump_filename):
			logger.debug(messages.REMOVING_EXISTING_DUMP_FILE)
			os.remove(target_dump_filename)

		dump_file_content = xml_to_string_pretty(dump_tree)
		save_backup_tar(dump_file_content, target_dump_filename)


def read_hosting_description_file(hosting_description_config):
	"""
	:type hosting_description_config: parallels.plesk.source.custom.connections.HostingDescriptionConfig
	"""
	input_formats_by_name = group_by_id(INPUT_FORMATS, lambda f: f.name)
	if hosting_description_config.file_format not in input_formats_by_name:
		raise MigrationError(
			messages.INVALID_FILE_FORMAT % hosting_description_config.path
		)
	return input_formats_by_name[hosting_description_config.file_format].read(hosting_description_config.path)


def guid_str():
	return str(uuid.uuid1())


class InputFormat(object):
	@property
	def name(self):
		raise NotImplementedError()

	def read(self, filename):
		raise NotImplementedError()


class InputFormatJSON(InputFormat):
	@property
	def name(self):
		return 'json'

	def read(self, filename):
		with open(filename) as fp:
			return json.loads(fp.read())


class InputFormatYAML(InputFormat):
	@property
	def name(self):
		return 'yaml'

	def read(self, filename):
		return read_yaml(filename)


class InputFormatXML(InputFormat):
	@property
	def name(self):
		return 'xml'

	def read(self, filename):
		root = ElementTree.parse(filename).getroot()

		def parse(node):
			list_nodes = {
				'clients': 'client',
				'subscriptions': 'subscription',
				'addon_domains': 'addon_domain',
				'subdomains': 'subdomain',
				'databases': 'database',
				'mailboxes': 'mailbox'
			}

			if node.tag in list_nodes:
				result = []

				for child in node:
					if child.tag != list_nodes[node.tag]:
						raise MigrationError(messages.INVALID_TAG % (child.tag, list_nodes[node.tag]))
					result.append(parse(child))

				return result
			else:
				if len(node) > 0:
					result = {}
					for child in node:
						result[child.tag] = parse(child)
					return result
				else:
					return node.text

		return parse(root)

INPUT_FORMATS = [InputFormatYAML(), InputFormatJSON(), InputFormatXML()]
