import logging
import re
import posixpath
from xml.etree import ElementTree
from parallels.common import MigrationError

from parallels.common.utils.plesk_cli_runner import PleskCLIRunnerCLI
from parallels.plesk_api import operator as plesk_ops
from parallels.common.utils.windows_utils import path_join as windows_path_join, get_from_registry
from parallels.common.utils import windows_utils
from parallels.utils import if_not_none, format_list, cached

logger = logging.getLogger(__name__)


def get_windows_vhosts_dir(runner):
	vhosts_dir = get_from_registry(
		runner,
		['HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Environment'],
		'plesk_vhosts'
	)

	if vhosts_dir is None:
		raise Exception(u'Failed to detect Plesk virtual hosts directory')

	vhosts_dir = vhosts_dir.strip('\\')
	logger.debug(u'Plesk virtual hosts directory is "%s"' % vhosts_dir)
	return vhosts_dir


def get_windows_plesk_dir(runner):
	plesk_dir = get_from_registry(
		runner,
		['HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Environment'],
		'plesk_dir'
	)

	if plesk_dir is None:
		raise Exception(u"Failed to detect Plesk base directory")

	plesk_dir = plesk_dir.strip('\\')
	logger.debug(u'Plesk base directory is "%s"' % plesk_dir)
	return plesk_dir


def get_windows_data_dir(runner):
	possible_registry_keys = [
		'HKLM\SOFTWARE\Wow6432Node\PLESK\PSA Config\Config',
		'HKLM\SOFTWARE\PLESK\PSA Config\Config'
	]
	data_dir = get_from_registry(runner, possible_registry_keys, 'PRODUCT_DATA_D')

	if data_dir is None:
		raise Exception(u"Failed to detect Plesk data directory")

	data_dir = data_dir.strip('\\')
	logger.debug(u'Plesk data directory is "%s"' % data_dir)
	return data_dir


def get_windows_dump_dir(runner):
	possible_registry_keys = [
		'HKLM\SOFTWARE\Wow6432Node\PLESK\PSA Config\Config',
		'HKLM\SOFTWARE\PLESK\PSA Config\Config'
	]
	dump_dir = get_from_registry(runner, possible_registry_keys, 'DUMP_D')

	dump_dir = dump_dir.strip('\\')
	logger.debug(u'Plesk dump directory is "%s"' % dump_dir)
	return dump_dir


def get_windows_vhost_dir(runner, vhost_name):
	vhosts_dir = get_windows_vhosts_dir(runner)

	vhost_name = vhost_name.encode('idna')
	vhost_dir = windows_path_join(vhosts_dir, vhost_name)
	logger.debug(u"Windows vhost directory: %s", vhost_dir)

	return vhost_dir


def get_unix_vhost_dir(runner, vhost_name):
	vhosts_dir = get_unix_vhosts_dir(runner)

	vhost_name = vhost_name.encode('idna')
	vhost_dir = posixpath.join(vhosts_dir, vhost_name)
	logger.debug(u"Unix vhost directory: %s", vhost_dir)

	return vhost_dir


def get_unix_vhost_system_dir(runner, vhost_name):
	vhosts_dir = get_unix_vhosts_dir(runner)

	vhost_name = vhost_name.encode('idna')
	vhost_dir = posixpath.join(vhosts_dir, 'system', vhost_name)
	logger.debug(u"Unix vhost system directory: %s", vhost_dir)

	return vhost_dir


def get_unix_vhosts_dir(runner):
	"""Get directory with virtual hosts on Plesk for Unix server"""
	return get_unix_conf_var(
		runner, 'HTTPD_VHOSTS_D', "Virtual hosts directory"
	)


def get_unix_mailnames_dir(runner):
	"""Get directory with mail messages on Plesk for Unix server"""
	return get_unix_conf_var(
		runner, '(PLESK|QMAIL)_MAILNAMES_D', 'Mail messages directory'
	)


def get_unix_dump_dir(runner):
	"""Get directory where Plesk stores backups on Plesk for Unix server"""
	return get_unix_conf_var(
		runner, 'DUMP_D', 'Backup dumps directory'
	)


def get_unix_product_root_dir(runner):
	"""Get directory where Plesk for Unix is installed"""
	return get_unix_conf_var(
		runner, 'PRODUCT_ROOT_D', 'Unix product root directory'
	)


def get_unix_conf_var(runner, var_name, description=None):
	"""Read variable from psa.conf config of Plesk for Unix

	Arguments:
	- runner - runner for Plesk for Unix server (from run_command module)
	- var_name - regular expression to match variable name
	- description - description of variable, just for debug logging

	Returns variable value.
	"""
	stdout = runner.run(
		'/bin/grep', ['-m1', '-E', '^\s*%s' % var_name, '/etc/psa/psa.conf']
	)
	var_value = stdout.strip().replace('\t', ' ').partition(' ')[2].strip().rstrip('/')
	if description is not None:
		logger.debug('%s: %s', description, var_value)
	else:
		logger.debug(
			u"Plesk for Unix configuration variable '%s' has value '%s'",
			var_name, var_value
		)
	return var_value


def get_windows_db_server_credentials(server, db_type, db_host):
	def _get_credentials(_server, _db_type, _db_host):
		with _server.runner() as _runner:
			exit_code, stdout, _ = _runner.sh_unchecked(
				ur'cmd.exe /C "{php} -dauto_prepend_file="" {cu} '
				ur'--get-credentials %s -type %s"' % (
					_db_host, _db_type
				),
				dict(
					php=ur"%s\admin\bin\php" % (server.plesk_dir,),
					cu=ur"%s\admin\plib\cu\database-registrar.php" % (server.plesk_dir,),
				)
			)
		if exit_code != 0:
			return None
		return stdout.strip()

	output = None
	if db_type == 'mssql' and db_host.startswith('.\\') and ':' not in db_host:
		# workaround for bug PPP-15779: port 0 specified in Plesk database for local MSSQL server,
		# installer automatically, so first try to check this case
		output = _get_credentials(server, db_type, '%s:0' % db_host)
	if output is None:
		output = _get_credentials(server, db_type, db_host)

	if output is None:
		raise Exception('Unable to retrive credentials for %s database server with host %s via %s' % (
			db_type, db_host, server.description()
		))

	xml = ElementTree.XML(output)
	return xml.findtext('username'), xml.findtext('password')


def get_unix_db_server_credentials(server, db_type, db_host):
	with server.runner() as runner:
		out = runner.sh(
			ur'{plesk_dir}/bin/sw-engine-pleskrun {plesk_dir}/admin/plib/api-cli/database-registrar.php '
			ur'--get-credentials %s -type %s' % (
				db_host, db_type
			),
			dict(
				plesk_dir=server.plesk_dir
			)
		).strip()
	xml = ElementTree.XML(out)
	return xml.findtext('username'), xml.findtext('password')


def convert_wildcard_to_path(domain_name):
	if domain_name.startswith('*'):
		return '_%s' % (domain_name[1:],)
	else:
		return domain_name


def get_plesk_version(runner):
	version_str = runner.run('cat', ['/usr/local/psa/version'])
	return tuple(map(int, re.split('[\. ]', version_str)[:2]))


def get_migrator_root_path(migrator_module):
	"""Get path to package root directory."""
	dirs = [p for p in migrator_module.__path__]
	assert all(d == dirs[0] for d in dirs)
	return dirs[0]


def set_mime_types(server, vhost_name, mime_types, vdir_name=None):
	"""Set MIME types of virtual host on Windows Plesk server

	Arguments:
	- server - instance of Windows target server (PPATargetServer or PleskTargetServer)
	- vhost_name - virtual host name (subscription or addon domain)
	- mime_types - dictionary of mime types (key - extension, value - mime type)
	- vdir_name - virtual directory to set mime type on; if None - mime types
		are set on whole server
	"""
	vhost_name = vhost_name.encode('idna')

	with server.runner() as runner:
		mimetypes_file = None
		if server.plesk_version >= (12, 0):
			mimetypes_file = server.get_session_file_path(
				'%s.mimetypes' % vhost_name
			)
			runner.upload_file_content(
				mimetypes_file,
				_mime_types_as_string(mime_types)
			)
			mimetypes_arg = mimetypes_file
		else:
			mimetypes_arg = _mime_types_as_string(mime_types)

		runner.sh(
			_get_webservermng_command(
				_get_webservermng_path(server),
				'set-mime-types',
				args={
					'vhost-name': vhost_name,
					'vdir-name': vdir_name,
					'mime-types': mimetypes_arg
				}
			)
		)

		if mimetypes_file is not None:
			runner.remove_file(mimetypes_file)


def get_mime_types(server, vhost_name, vdir_name=None):
	"""Get MIME types of virtual host on Windows Plesk server

	Arguments:
	- server - instance of Windows target server (PPATargetServer or PleskTargetServer)
	- vhost_name - virtual host name (subscription or addon domain)
	- vdir_name - virtual directory name
	"""
	vhost_name = vhost_name.encode('idna')
	with server.runner() as runner:
		mime_types_str = runner.sh(
			_get_webservermng_command(
				_get_webservermng_path(server),
				'get-mime-types',
				args={	
					'vhost-name': vhost_name,
					'vdir-name': vdir_name,
				}
			)
		)
	return _parse_mime_types_string(mime_types_str)


def get_error_documents(server, vhost_name, vdir_name=None):
	"""Get error documents of virtual host on Windows Plesk server

	Arguments:
	- server - instance of Windows target server (PPATargetServer or PleskTargetServer)
	- vhost_name - virtual host name (subscription or addon domain)
	- vdir_name - virtual directory name
	"""
	vhost_name = vhost_name.encode('idna')

	with server.runner() as runner:
		error_documents_str = runner.sh(
			_get_webservermng_command(
				_get_webservermng_path(server),
				'get-error-docs',
				args={	
					'vhost-name': vhost_name,
					'vdir-name': vdir_name
				}
			)
		)
	return error_documents_str


def get_vdir_info(server, vhost_name):
	websrvmng_path = windows_path_join(server.plesk_dir, ur'admin\bin\websrvmng')
	with server.runner() as runner:
		vdir_info = runner.sh('{websrvmng_path} --list-vdirs --vhost-name={vhost_name}', dict(
			websrvmng_path=websrvmng_path, vhost_name=vhost_name.encode('idna')
		))
	return vdir_info


def _mime_types_as_string(mime_types):
	return u"".join([ 
		u"%s=%s;" % (ext, mime_type) 
		for ext, mime_type in mime_types.iteritems() 
	])


def _parse_mime_types_string(mime_types_string):
	mime_types = {}
	for mime_type_str in mime_types_string.split(';'):
		mime_type_str = mime_type_str.strip()
		if mime_type_str == '':
			continue
		ext, mime_type = mime_type_str.split('=')
		mime_types[ext] = mime_type
	return mime_types


def _get_webservermng_path(server):
		target_plesk_dir = server.plesk_dir
		return windows_path_join(
			target_plesk_dir, ur'admin\bin\websrvmng'
		)


def _get_webservermng_command(websrvmng_path, action, args):
	command = '{websrvmng_path} --{action}'.format(
		websrvmng_path=windows_utils.quote_arg(
			websrvmng_path
		),
		action=action
	)

	for key, value in args.iteritems():
		if value is not None:
			command += ' --{key}={value}'.format(
				key=key,
				value=windows_utils.quote_arg(value)
			)

	return command


def convert_wildcard_domain_path(domain_path):
	"""Convert wildcard domain path: in domain physical path '*' symbol replaced to '_'.

	Arguments:
	- domain_path - vhost path to domain
	"""
	return domain_path.replace('*', '_')


def get_apache_restart_interval(target_server):
	"""
	:type target_server: parallels.common.connections.target_servers.TargetServer
	"""
	with target_server.runner() as runner:
		return int(runner.sh('/usr/local/psa/bin/server_pref --get-apache-restart-interval'))


def set_apache_restart_interval_value(target_server, new_value):
	"""
	:type target_server: parallels.common.connections.target_servers.TargetServer
	"""
	with target_server.runner() as runner:
		runner.sh('/usr/local/psa/bin/server_pref -u -restart-apache {new_value}', dict(new_value=new_value))


def restart_plesk_apache(runner, plesk_dir=None):
	"""
	:type runner: parallels.common.run_command.BaseRunner
	"""
	if plesk_dir is None:
		plesk_dir = get_unix_product_root_dir(runner)
	runner.sh("%s/admin/bin/websrvmng -r" % plesk_dir)


def get_plesk_ips_with_cli(plesk_server):
	"""Get information about IP addresses on specified Plesk server with Plesk CLI

	:type plesk_server: parallels.common.connections.plesk_server.PleskServer
	:rtype: list[parallels.common.utils.plesk_utils.IPAddressInfo]
	"""
	runner = PleskCLIRunnerCLI(plesk_server)
	out = runner.run('ipmanage', ['--xml-info'])
	xml = ElementTree.XML(out)
	ip_addresses = []
	for ip_node in xml.findall('ip'):
		public_ip_address = ip_node.findtext('publicIp')
		if public_ip_address.strip() == '':
			public_ip_address = None
		ip_addresses.append(IPAddressInfo(
			ip_type=ip_node.findtext('type'),
			ip_address=ip_node.findtext('ip_address'),
			public_ip_address=public_ip_address,
			hostings=if_not_none(ip_node.findtext('hostings'), int),
			clients=if_not_none(ip_node.findtext('clients'), int)
		))

	return ip_addresses


def get_plesk_ips_with_api(plesk_server):
	"""Get information about IP addresses on specified Plesk server with Plesk API

	:type plesk_server: parallels.common.connections.plesk_server.PleskServer
	:rtype: list[parallels.common.utils.plesk_utils.IPAddressInfo]
	"""
	ips = plesk_server.plesk_api().send(plesk_ops.IpOperator.Get()).data
	ip_addresses = []

	for ip_response in ips:
		ip_addresses.append(IPAddressInfo(
			ip_type=ip_response.ip_type,
			ip_address=ip_response.ip_address,
			public_ip_address=ip_response.public_ip_address,
			hostings=0  # can't determine with Plesk API, just set to zero
		))

	return ip_addresses


def refresh_node_components(node):
	"""
	:type node: parallels.common.connections.plesk_server.PleskServer
	"""
	with node.runner() as runner:
		runner.sh(ur'"%s\admin\bin\defpackagemng" --get --force' % node.plesk_dir)


def change_dedicated_iis_app_pool_state(plesk_server, subscription_name, state):
	PleskCLIRunnerCLI(plesk_server).run(
		'subscription',
		[
			'--set-iis-app-pool-settings',
			subscription_name.encode('idna'),
			'-iis-app-pool-turned-on',
			'true' if state else 'false'
		],
		dict(PLESK_RESTORE_MODE='1')
	)


class IPAddressInfo(object):
	def __init__(self, ip_type, ip_address, public_ip_address, hostings=0, clients=0):
		self.ip_type = ip_type
		self.ip_address = ip_address
		self.public_ip_address = public_ip_address if public_ip_address is not None else ip_address

		if hostings is not None:
			self.hostings = hostings
		else:
			self.hostings = 0

		if clients is not None:
			self.clients = clients
		else:
			self.clients = 0

	@property
	def is_completely_free(self):
		"""Check if IP address is completely free - not used by anyone

		:rtype: bool
		"""
		return self.clients == 0 and self.hostings == 0

	def __repr__(self):
		return "IPAddressInfo(ip_type=%r, ip_address=%r, public_ip_address=%r, hostings=%r, clients=%r)" % (
			self.ip_type, self.ip_address, self.public_ip_address, self.hostings, self.clients
		)

@cached
def get_database_subscription(panel_server, db_name, db_type, db_host):
	"""Get name of subscription which owns specified database. If database does not exist - return None

	:type panel_server: parallels.common.connections.plesk_server.PleskServer
	:type db_name: basestring
	:type db_type: basestring
	:type db_host: basestring
	:rtype: basestring | None
	"""
	query = """
		SELECT domains.name FROM data_bases
			JOIN DatabaseServers ON data_bases.db_server_id = DatabaseServers.id
			JOIN domains on data_bases.dom_id = domains.id
			WHERE
				data_bases.name = '{db_name}' AND
				DatabaseServers.host= '{db_host}' AND
				DatabaseServers.type = '{db_type}'
	""".format(db_name=db_name, db_type=db_type, db_host=db_host)
	results = _query_plesk_db(panel_server, query)
	if len(results) == 0:
		return None
	elif len(results) == 1:
		return results[0][0]
	else:
		raise MigrationError(
			"Panel server '%s' has inconsistency in its database. "
			"Customer's database '%s' of type '%s' on host '%s' is owned by more than 1 webspace: %s" % (
				panel_server.description(), db_name, db_type, db_host, format_list(results)
			)
		)


def _query_plesk_db(panel_server, query):
	"""
	:type panel_server: parallels.common.connections.plesk_server.PleskServer
	:type query: string
	"""
	if panel_server.is_windows():
		command = ur'%s\admin\bin\dbclient --direct-sql --sql="{query}"' % panel_server.plesk_dir
		query = " ".join([line.strip() for line in query.split()])
	else:
		command = u"mysql --silent --skip-column-names -u admin -p`cat /etc/psa/.psa.shadow` psa -e \"{query}\""

	with panel_server.runner() as runner:
		lines = runner.sh(command.format(query=query)).splitlines()

	if len(lines) == 0:
		return []

	if panel_server.is_windows():
		# the first line is a header line
		data_lines = lines[1:]
	else:
		# column names are skipped with an option, so data starts from the 1st line
		data_lines = lines

	results = []
	for line in data_lines:
		if line.strip() != '':
			line_data = line.strip().split()
			results.append([d.strip() for d in line_data])

	return results