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.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 plesk_utils, unix_utils, ssh_utils
from parallels.common.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=[]):
	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=[], skip_if_source_dir_not_exists=False):
	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)
	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))

def copy_file_content_unix(source_ip, source_user, runner_source, runner_target, source_path, target_path, key_pathname):
	_copy_content_unix(source_ip, source_user, runner_source, runner_target, source_path, target_path, key_pathname, content_type = 'f')

def detect_rsync_path(runner, node_info_string):
	default_rsync_path = '/usr/bin/rsync'
	if unix_utils.file_exists(runner, default_rsync_path):
		return None
	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=[]):
	cmd = '/usr/bin/rsync'
	args = [ 
		"-e", "ssh -i {key} -o StrictHostKeyChecking=no".format(key=key_pathname),
		"--archive",
		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]
	rsync_path = detect_rsync_path(source_runner, source_ip) # detect rsync binary location on the source node
	if rsync_path is not None: # non-default path
		args = ["--rsync-path=%s" % (rsync_path,)] + 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 = []):
	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)
	runner_target.run(cmd, args)

	if unix_utils.is_debian(runner_source) and unix_utils.is_centos(runner_target):
		logger.debug(u"Change file permissions so www-data user and group (Debian) are converted to apache (CentOS)")
		unix_utils.map_copy_owner_user(runner_source, runner_target, source_path, target_path, 'www-data', 'apache')
		unix_utils.map_copy_owner_group(runner_source, runner_target, source_path, target_path, 'www-data', 'apache')

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
		)

def copy_db_content_linux(runner_source, runner_target, src_server_ip, src, dst, db_name, source_dump_filename='/tmp/db_backup.sql', target_dump_filename='/tmp/db_backup.sql'):
	"""
	Arguments
	- src - instance of DbServerInfo class describing the source database server
	- dst - instance of DbServerInfo class describing the target database server
	"""
	if src.dbtype == 'mysql':
		backup_command = u"mysqldump -h {src_host} -P {src_port} -u{src_admin} --quick --quote-names --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
	))

	with ssh_utils.public_key_ssh_access_runner(runner_target, runner_source) as key_pathname: # copy backup to target node
		runner_target.sh(u"scp -i {key_pathname} -o StrictHostKeyChecking=no {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
		))
	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

@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. 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)

	files_to_delete = windows_path_join(vhost_dir, 'httpdocs', "*.*")
	# Clean up directory:
	# first, delete all files in a directory 
	runner.sh(
		'cmd /c del {files_to_delete} /s /q', 
		dict(files_to_delete=files_to_delete)
	)
	# then remove all directories (as 'del' does not delete directories,
	# 'rmdir' does not delete files)
	runner.sh(
		u'cmd /c for /d %p in ({files_to_delete}) do rmdir "%p" /s /q', 
		dict(files_to_delete=files_to_delete)
	)

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