import logging
import os
import sys
import datetime
import posixpath
from parallels import utils
from functools import wraps
from contextlib import contextmanager
from collections import namedtuple

from parallels.utils.logging import BleepingLogger
from parallels.common.utils.windows_utils import check_windows_national_symbols_command
from parallels.common.utils.windows_utils import path_join as windows_path_join
from parallels.common import MigrationError, MigrationNoContextError
from parallels.common.utils import ssh_utils, unix_utils, plesk_utils
from parallels.common.utils.config_utils import get_option
from parallels.common.logging_context import log_context
from parallels.common.utils.steps_profiler import get_default_steps_profiler

logger = logging.getLogger(__name__)


def copy_vhost_content_unix(
	source_ip, source_user, runner_source, runner_target,
	subscription_name, key_pathname, skip_if_source_dir_not_exists=False, exclude=None
):
	if exclude is None:
		exclude = []

	source_vhost_dir = plesk_utils.get_unix_vhost_dir(runner_source, subscription_name)
	if unix_utils.file_exists(runner_source, source_vhost_dir):
		target_vhost_dir = plesk_utils.get_unix_vhost_dir(runner_target, subscription_name)
		copy_directory_content_unix(
			source_ip, source_user, runner_source, runner_target,
			source_vhost_dir, target_vhost_dir, key_pathname, exclude
		)
	else:
		if skip_if_source_dir_not_exists:
			# source virtual host directory does not exists, so there is nothing to copy 
			return
		else:
			raise MigrationError(
				"Error while copying virtual host's content: "
				"virtual host's directory '%s' does not exist on source server '%s'" % (
					source_vhost_dir, source_ip
				)
			)


def copy_vhost_system_content_unix(
	source_ip, source_user, runner_source, runner_target, subscription_name, key_pathname
):
	source_vhost_dir = plesk_utils.get_unix_vhost_system_dir(runner_source, subscription_name)
	target_vhost_dir = plesk_utils.get_unix_vhost_system_dir(runner_target, subscription_name)

	copy_directory_content_unix(
		source_ip, source_user, runner_source, runner_target, source_vhost_dir, target_vhost_dir, key_pathname
	)


def copy_directory_content_unix(
	source_ip, source_user, runner_source, runner_target, source_path, target_path, 
	key_pathname, exclude=None, skip_if_source_dir_not_exists=False,
	rsync_additional_args=None,
	user_mapping=None,
	group_mapping=None,
	source_rsync_bin=None,
):
	"""
	:type user_mapping: dict[basestring, basestring]
	:type group_mapping: dict[basestring, basestring]
	"""
	if exclude is None:
		exclude = []
	if user_mapping is None:
		user_mapping = {}
	if group_mapping is None:
		group_mapping = {}

	if unix_utils.file_exists(runner_source, source_path):
		_copy_content_unix(
			source_ip, source_user, runner_source, runner_target, source_path, target_path, 
			key_pathname, content_type='d', exclude=exclude, 
			rsync_additional_args=rsync_additional_args,
			user_mapping=user_mapping,
			group_mapping=group_mapping,
			source_rsync_bin=source_rsync_bin,
		)
	else:
		if skip_if_source_dir_not_exists:
			# source directory does not exists, so there is nothing to copy 
			return
		else:
			raise MigrationError(
				"Error while copying content: directory '%s' does not exist on source server '%s'" % (
					source_path, source_ip
				)
			)

DEFAULT_RSYNC_PATH = '/usr/bin/rsync'


def copy_db_content_linux(runner_source, runner_target, src_server_ip, src, dst, db_name):
	"""
	Arguments
	- src - instance of DbServerInfo class describing the source database server
	- dst - instance of DbServerInfo class describing the target database server
	"""

	source_dump_filename = '/tmp/db_backup.sql'
	target_dump_filename = '/tmp/db_backup.sql'

	if src.dbtype == 'mysql':
		backup_command = u"mysqldump -h {src_host} -P {src_port} -u{src_admin} --quick --quote-names " \
			u"--add-drop-table --default-character-set=utf8 --set-charset {db_name}"
		# workaround for Plesk feature - it does not tell default MySQL server's admin password
		if src.host == 'localhost' and src.port == 3306:
			backup_command += u" -p\"`cat /etc/psa/.psa.shadow`\""
		else:
			backup_command += u" -p{src_password}"

		restore_command = u"mysql --no-defaults -h {dst_host} -P {dst_port} -u{dst_admin} {db_name}"
		# workaround for Plesk feature - it does not tell default MySQL server's admin password
		if dst.host == 'localhost' and dst.port == 3306:
			restore_command += u" -p\"`cat /etc/psa/.psa.shadow`\""
		else:
			restore_command += u" -p{dst_password}"
	elif src.dbtype == 'postgresql':
		backup_command = u"PGUSER={src_admin} PGPASSWORD={src_password} PGDATABASE={db_name} pg_dump -Fc -b -O -i"
		if src.host != 'localhost':
			backup_command += " -h {src_host} -p {src_port}"
		restore_command = u"PGUSER={dst_admin} PGPASSWORD={dst_password} pg_restore -v -d {db_name} -c"
		if dst.host != 'localhost':
			restore_command += " -h {dst_host} -p {dst_port}"
		if runner_source.run_unchecked('which', ['pg_dump'])[0] != 0:
			raise MigrationError(
				"pg_dump utility is not installed on '%s' server. "
				"Please make sure that:\n"
				"1) All external PostgreSQL servers (other than Plesk servers) are specified in config.ini "
				"in 'external-postgresql-servers' option, and pg_dump utility is installed on them.\n"
				"OR\n"
				"2) pg_dump is installed on all Plesk servers that have domains with PostgreSQL databases.\n"
				"Once the problem is fixed, run 'copy-db-content' migration tool command"
				"to copy only the databases" % src_server_ip
			)
	else:
		raise Exception(u"Database has unsupported type '%s' and hence will not be copied" % src.db_type)

	runner_source.sh(backup_command + u" > {source_dump_filename}", dict(  # backup database on source node
		src_host=src.host,
		src_port=src.port,
		src_admin=src.login,
		src_password=src.password,
		db_name=db_name,
		source_dump_filename=source_dump_filename
	))

	def copy_dump_file(key_pathname):
		runner_target.sh(
			u"scp -i {key_pathname} -o StrictHostKeyChecking=no -o GSSAPIAuthentication=no "
			u"{src_server_ip}:{source_dump_filename} {target_dump_filename}", dict(
				key_pathname=key_pathname,
				src_server_ip=src_server_ip,
				source_dump_filename=source_dump_filename,
				target_dump_filename=target_dump_filename
			)
		)
	with ssh_utils.public_key_ssh_access_runner(runner_target, runner_source) as key_pathname:
		copy_dump_file(key_pathname)

	runner_source.remove_file(source_dump_filename)  # remove backup from source node
	runner_target.sh(restore_command + u" < {target_dump_filename}", dict(  # restore backup on target node
		dst_host=dst.host, dst_port=dst.port,
		dst_admin=dst.login, dst_password=dst.password,
		db_name=db_name,
		target_dump_filename=target_dump_filename,
	))
	runner_target.remove_file(target_dump_filename)  # remove backup from target node


def detect_rsync_path(runner, node_info_string):
	if unix_utils.file_exists(runner, DEFAULT_RSYNC_PATH):
		return DEFAULT_RSYNC_PATH
	else:
		hsphere_rsync_path = '/hsphere/shared/bin/rsync'
		if unix_utils.file_exists(runner, hsphere_rsync_path):
			return hsphere_rsync_path
		else:
			raise MigrationError("Unable to find rsync binary on the node %s. Checked the following paths: %s, %s" % (
					node_info_string, DEFAULT_RSYNC_PATH, hsphere_rsync_path
				)
			)


def create_rsync_command(
	key_pathname, source_runner, source_user, source_ip, 
	source_filepath, target_filepath, exclude=None,
	rsync_additional_args=None,
	source_rsync_bin=None
):
	if exclude is None:
		exclude = []

	cmd = '/usr/bin/rsync'

	if rsync_additional_args is None:
		rsync_additional_args = []

	args = [ 
		"-e", "ssh -i {key} -o StrictHostKeyChecking=no -o GSSAPIAuthentication=no".format(key=key_pathname),
		"--archive"
	] + rsync_additional_args + [
		u"{source_user}@{source_ip}:{source_filepath}".format(
			source_user=source_user,
			source_ip=source_ip,
			source_filepath=source_filepath
		),
		u"%s" % (target_filepath,),
	] + [u"--exclude=%s" % ex for ex in exclude]

	if source_rsync_bin is None:
		# detect rsync binary location on the source node
		source_rsync_bin = detect_rsync_path(
			source_runner, source_ip
		)
		if source_rsync_bin != DEFAULT_RSYNC_PATH: # non-default path
			args = ["--rsync-path=%s" % (source_rsync_bin,)] + args

	return cmd, args


def _copy_content_unix(
	source_ip, source_user, runner_source, runner_target, 
	source_path, target_path, key_pathname, content_type='d',
	exclude=None, rsync_additional_args=None,
	user_mapping=None, group_mapping=None,
	source_rsync_bin=None
):
	"""
	:type user_mapping: dict[basestring, basestring]
	:type group_mapping: dict[basestring, basestring]
	"""
	if exclude is None:
		exclude = []
	if user_mapping is None:
		user_mapping = {}
	if group_mapping is None:
		group_mapping = {}

	if content_type == 'd':
		source_path += '/'
		target_path += '/'

	cmd, args = create_rsync_command(
		key_pathname, runner_source, source_user, 
		source_ip, source_path, target_path, exclude,
		rsync_additional_args,
		source_rsync_bin
	)
	runner_target.run(cmd, args)

	for source_user, target_user in user_mapping.iteritems():
		logger.debug(u"Change file permissions according to user map")
		unix_utils.map_copy_owner_user(
			runner_source, runner_target, source_path, target_path, source_user, target_user
		)
	for source_group, target_group in group_mapping.iteritems():
		logger.debug(u"Change file permissions according to group map")
		unix_utils.map_copy_owner_group(
			runner_source, runner_target, source_path, target_path, source_group, target_group
		)


def vhost_dir_exists_unix(runner, vhost_name):
	vhost_dir = plesk_utils.get_unix_vhost_dir(runner, vhost_name)
	exit_code, _, _ = runner.run_unchecked("/usr/bin/test",  ["-d", vhost_dir])
	return exit_code == 0


def vhost_system_dir_exists_unix(runner, vhost_name):
	vhost_system_dir = plesk_utils.get_unix_vhost_system_dir(runner, vhost_name)
	exit_code, _, _ = runner.run_unchecked("/usr/bin/test",  ["-d", vhost_system_dir])
	return exit_code == 0


def trace(op_name, message):
	def decorator(method):
		@wraps(method)
		def wrapper(self, *args, **kw):
			with trace_step(op_name, message, profile=True):
				return method(self, *args, **kw)
		return wrapper
	return decorator


@contextmanager
def trace_step(op_name, message, profile=False, log_level='info', compound=True):
	if log_level == 'info':
		log_function = logger.info
	elif log_level == 'debug':
		log_function = logger.debug
	else:
		raise Exception("Invalid log level '%s'" % log_level)

	if compound:
		log_function(u"")	# put an empty line before the block
		log_function(u"START: %s", message)
	else:
		log_function(message)
	try:
		with log_context(op_name):
			if profile:
				with get_default_steps_profiler().measure_time(
					op_name, message
				):
					yield
			else:
				yield
		if compound:
			log_function(u"FINISH: %s", message)
	except MigrationNoContextError:
		if compound:
			log_function(u"FINISH: %s", message)
		raise
	except:
		logger.error(u"ABORT (by exception): %s", message)
		raise


class DbServerInfo(namedtuple('DbServerInfo', ('dbtype', 'host', 'port', 'login', 'password'))):
	@staticmethod
	def from_plesk_api(db_server_info):
		"""Convert from plesk_api.DbServerInfo to DbServerInfo"""
		return DbServerInfo(
			dbtype=db_server_info.dbtype,
			host=db_server_info.host,
			port=db_server_info.port,
			login=db_server_info.admin,
			password=db_server_info.password
		)

	@staticmethod
	def from_plesk_backup(db_server_info):
		"""Convert plesks_migrator.data_model.DatabaseServer to DbServerInfo"""
		return DbServerInfo(
			dbtype=db_server_info.dbtype,
			host=db_server_info.host,
			port=db_server_info.port,
			login=db_server_info.login,
			password=db_server_info.password
		)


@contextmanager
def plain_passwords_enabled(ppa_runner):
	def run_not_critical_command(cmd, description):
		exit_code, stdout, stderr = ppa_runner.sh_unchecked(cmd)
		if exit_code != 0:
			logger.error(u"Failed to %s. Command: %s, stdout: %s, stderr: %s", description, cmd, stdout, stderr)

	get_value_cmd = u"/usr/local/psa/bin/settings -g allow_api_show_plain_passwords"
	exit_code, plain_passwords_enabled, stderr = ppa_runner.sh_unchecked(get_value_cmd)
	plain_passwords_enabled = plain_passwords_enabled.strip()

	if exit_code != 0:
		logger.error(
			u"Failed to determine if PPA Plesk API is allowed to show plain passwords. "
			u"Command: %s, stdout: %s, stderr: %s",
			get_value_cmd, plain_passwords_enabled, stderr
		)
		plain_passwords_enabled = 'false'

	if plain_passwords_enabled != 'true':
		run_not_critical_command(
			"/usr/local/psa/bin/settings -s allow_api_show_plain_passwords=true",
			u"allow PPA Plesk API showing plain passwords"
		)
	try:
		yield
	finally:
		if plain_passwords_enabled != 'true':
			run_not_critical_command(
				"/usr/local/psa/bin/settings -d allow_api_show_plain_passwords",
				u"disable PPA Plesk API showing plain passwords"
			)


def format_report(report, minor_issues=None, report_file_path=None, show_no_issue_branches=True):
	"""Print issues report to stdout as tree with the use of pseudographics.
	show_no_issue_branches - whether to show the branches without issues
	minor_issues - do not show the no-issue nodes of the specified types, even if show_no_issue_branches is True
	"""
	if minor_issues is None:
		minor_issues = set([])

	def format_issue(issue):
		return u"%s: %s\n%s" % (issue.problem.severity, issue.problem.description, issue.solution)

	def report_to_tree(report):
		def keep_report(report):
			if not show_no_issue_branches and not report.has_issues():
				return False
			if (report.type in minor_issues and len(report.children) == 0 and len(report.issues) == 0):
				return False
			return True

		return (
			u"%s '%s'" % (report.type, report.name) if report.name is not None else report.type,
			[
				item for item in
					[(format_issue(i), []) for i in report.issues] + 
					[report_to_tree(c) for c in report.children.values() if keep_report(c)]
			]
		)
	tree = report_to_tree(report)

	if sys.stdout.encoding == 'UTF-8':
		tree_symbols = utils.utf8_tree_symbols
	else:
		tree_symbols = utils.ascii_tree_symbols
	tree_report = utils.draw_tree(tree, spacing=1, symbols=tree_symbols)
	return tree_report


def save_report(tree_report, report_file_path):
	report_file_path = u'%s%s' % (report_file_path, datetime.datetime.now().strftime(".%Y.%m.%d.%H.%M.%S"))
	with open(report_file_path, 'w') as f:
		f.write(tree_report.encode('utf-8'))
	return report_file_path


def _make_winexe_command(access_data, remote_command):
	check_windows_national_symbols_command(remote_command)
	return [
		u"winexe", 
		u"--debug-stderr",
		u"--reinstall",
		u"-U", "%s%%%s" % (access_data.windows_auth.username, access_data.windows_auth.password),
		u"//%s" % (access_data.ip,),
		remote_command
	]


def clean_up_vhost_dir(subscription_name, runner, is_windows):
	"""Remove all garbage in subscription's virtual host directory. This
	garbage is created because of default virtual host skeleton. This could
	cause problems.  For example, skeleton contains httpdocs/index.html page.
	We run rsync without --delete option during content migration. If there is
	no index.html on source node, index.html from skeleton is left as is.  So,
	if there was index.php page on source node, which usually have less
	priority than index.html, then main page of a site is replaced with
	index.html template (if you type http://example.com/ after migration, you
	will get http://example.com/index.html, but not
	http://example.com/index.php as before migration). So we should remove the
	garbage. Better way - ask Plesk to implement ability to create site with
	"empty" skeleton.
	"""

	if is_windows:
		_clean_up_windows_vhost_dir(subscription_name, runner)
	else:
		_clean_up_unix_vhost_dir(subscription_name, runner)


def _clean_up_unix_vhost_dir(subscription_name, runner):
	vhost_dir = plesk_utils.get_unix_vhost_dir(runner, subscription_name)

	full_dir_to_clean_path = posixpath.join(vhost_dir, 'httpdocs') 
	runner.run("/usr/bin/find", [
		full_dir_to_clean_path, 
		# not to include the directory itself, as find puts '.' if mindepth is
		# not defined
		"-mindepth", "1",
		# so find won't fail with '/usr/bin/find:
		# /var/www/vhosts/a.tld/httpdocs/b/c.html: No such file or directory'
		# when it removes directory and only then tries to remove files inside
		# the directory 
		"-maxdepth", "1", 
		"-exec", "rm", "-rf", "{}", "+"]
	)


def _clean_up_windows_vhost_dir(subscription_name, runner):
	vhost_dir = plesk_utils.get_windows_vhost_dir(runner, subscription_name)
	runner.clean_directory(windows_path_join(vhost_dir, 'httpdocs'))


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


def get_package_extras_file_path(package, filename):
	"""Get path to extras file of Python package"""
	return os.path.join(
		get_package_root_path(package), 'extras', filename
	)


def get_package_scripts_file_path(package, filename):
	"""Get path to scripts file of Python package"""
	return os.path.join(
		get_package_root_path(package), 'scripts', filename
	)


def version_to_tuple(version):
	return tuple(map(int, (version.split("."))))


def split_unix_path(path):
	"""Split path by directory name and file name

	Returns tuple (directory name, file name)

	:rtype tuple
	"""
	last_slash_pos = path.rfind('/')
	directory = path[:last_slash_pos]
	filename = path[last_slash_pos+1:]
	return directory, filename


def normalize_domain_name(domain_name):
	"""Normalize domain name - convert to lowercase punycode

	:rtype basestring
	"""
	return domain_name.encode('idna').lower()


@contextmanager
def upload_temporary_file_content(runner, filename, content):
	"""Upload contents to temporary file on remote server"""
	runner.upload_file_content(filename, content)
	yield
	runner.remove_file(filename)


def get_customer_name_in_report(customer):
	name = customer.login
	if customer.personal_info is not None:
		details = ', '.join(
			x for x in [
				customer.personal_info.first_name,
				customer.personal_info.last_name,
				customer.personal_info.email
			] if x is not None and x != ''
		)
		if details != "":
			name += " (%s)" % details

	return name


def get_bleeping_logger(logger):
	"""Create and return enhanced logger object."""
	default_context_length = 10
	log_context_length = get_option(
		'GLOBAL', 'log-message-context-length', default_context_length)
	return BleepingLogger(logger, context_length=log_context_length)