import os

from parallels.core import messages
import logging
import ntpath
from collections import namedtuple

from parallels.core import MigrationError
from parallels.core.utils import download
from parallels.core.utils import windows_utils
from parallels.core.utils.common.threading_utils import LocksByKey
from parallels.core.utils.steps_profiler import sleep
from parallels.core.utils import windows_thirdparty
from parallels.core.utils.windows_utils import cmd_command
from parallels.core.utils.windows_utils import file_exists

logger = logging.getLogger(__name__)

RsyncConfig = namedtuple('RsyncConfig', (
	'client_ips', 'vhosts_dir', 'migrator_dir', 'user', 'password'
))


install_locks = LocksByKey()


class RsyncInstaller(object):
	"""Install rsync on remote Windows node"""

	def __init__(self, server):
		self.server = server

	def install(self):
		base_path = self.server.get_session_file_path(ur'rsync\bin')

		with self.server.runner() as runner, install_locks.lock(self.server):
			if file_exists(runner, ntpath.join(base_path, 'panel-migrator-rsync.exe')):
				logger.debug(
					messages.RSYNC_IS_ALREADY_INSTALLED_SERVER_SKIP, 
					self.server.get_session_file_path('rsync')
				)
			else:
				storage_path = windows_thirdparty.get_rsync_installer_bin()
				if os.path.exists(storage_path) is False:
					storage_url = 'http://autoinstall.plesk.com/panel-migrator/thirdparties/panel-migrator-rsync-installer.exe'
					logger.info(messages.DOWNLOAD_RSYNC.format(url=storage_url))
					download(storage_url, storage_path)
				installer_path = self.server.get_session_file_path('panel-migrator-rsync-installer.exe')
				logger.info(messages.UPLOAD_RSYNC.format(path=installer_path, server=self.server.description()))
				runner.upload_file(windows_thirdparty.get_rsync_installer_bin(), installer_path)
				logger.info(messages.INSTALL_RSYNC.format(server=self.server.description()))
				runner.sh(cmd_command(
					r'"%s" -o"%s" -y' % (
						installer_path,
						self.server.session_dir()
					)
				))

		return base_path


class RsyncInstance(object):
	def __init__(self, base_path):
		self.base_path = base_path


class RsyncClient(RsyncInstance):
	pass


class RsyncServer(RsyncInstance):
	def __init__(self, base_path, server):
		super(RsyncServer, self).__init__(base_path)
		self._config = None
		self._server = server

	def configure(self, config):
		with self._server.runner() as runner:
			logger.info(messages.CONFIGURE_RSYNC_SERVER_RSYNCDCONF_AND_RSYNCDSECRETS)
			runner.sh(cmd_command(
				'cd "{rsync_dir}"&&configure.bat {source_servers} {vhosts_dir} {migrator_dir} {user} {password}'.format(
					rsync_dir=self.base_path,
					source_servers=config.client_ips,
					vhosts_dir=windows_utils.convert_path_to_cygwin(
						config.vhosts_dir
					) if config.vhosts_dir is not None else '/cygdrive/c',  # XXX
					migrator_dir=windows_utils.convert_path_to_cygwin(
						config.migrator_dir
					),
					user=config.user,
					password=config.password,
				)
			))

		self._config = config

	def restart(self):
		with self._server.runner() as runner:
			logger.info(messages.STOP_RSYNC_IF_IT_IS_RUNNING)
			runner.sh(cmd_command(
				r'cd "{rsync_dir}"&&restart.bat'.format(
					rsync_dir=self.base_path,
				)
			))

	def stop(self):
		with self._server.runner() as runner:
			"""Stop rsync server"""
			runner.sh(cmd_command(
				r'cd "{rsync_dir}"&&stop.bat'.format(
					rsync_dir=self.base_path,
				)
			))

	def set_vhosts_dir(self, vhosts_dir):
		if self._config is None:
			raise Exception(messages.UNABLE_SET_VHOSTS_DIR_RSYNC_SERVER % self.base_path)
		new_config = RsyncConfig(
			client_ips=self._config.client_ips,
			vhosts_dir=vhosts_dir,
			migrator_dir=self._config.migrator_dir,
			user=self._config.user,
			password=self._config.password
		)
		self.configure(new_config)

	@property
	def vhosts_dir(self):
		return self._config.vhosts_dir if self._config is not None else None

	@property
	def login(self):
		return self._config.user if self._config is not None else None

	@property
	def password(self):
		return self._config.password if self._config is not None else None


class RsyncControl(object):
	"""Simple object to work with provided rsync client and rsync server"""
	def __init__(self, target, target_rsync_bin, source_ip, source_login, source_password, server=None):
		self._target = target
		self._target_rsync_bin = target_rsync_bin
		self._source_ip = source_ip
		self._source_login = source_login
		self._source_password = source_password
		self._server = server
		self._max_attempts = 5
		self._attempts_before_restart = 2
		self._interval_between_attempts = 10

	@staticmethod
	def get_rsync_path_to_source_session_dir_file(filename):
		"""Get path that could be used by rsync commands for a file located in session directory on the source server

		:param str filename: path to a file on a source server relative to session directory
		:rtype: str
		"""
		return "migrator\%s" % filename

	def sync(self, source_path, target_path, exclude=None, rsync_additional_args=None):
		if self._source_password is not None:
			set_password = u'set RSYNC_PASSWORD=%s&&' % (self._source_password,)
		else:
			set_password = u''

		if self._source_login is not None:
			login_clause = '%s@' % (self._source_login,)
		else:
			login_clause = ''

		additional_args = ""
		if rsync_additional_args is not None and len(rsync_additional_args) > 0:
			additional_args += u" ".join([arg for arg in rsync_additional_args]) + " "

		cmd = cmd_command(
			set_password +
			u'{rsync_bin} --no-perms -t -r %srsync://%s{source_ip}/{source_path} {target_path}' % (
				additional_args, login_clause,
			)
		)
		if exclude is not None and len(exclude) > 0:
			cmd += u"".join([u" --exclude %s" % ex for ex in exclude])

		args = dict(
			rsync_bin=self._target_rsync_bin,
			source_ip=self._source_ip,
			source_path=source_path,
			target_path=target_path,
		)

		return self._run_command(cmd, args)

	def upload(self, source_path, target_path):
		if self._source_password is not None:
			set_password = u'set RSYNC_PASSWORD=%s&&' % (self._source_password,)
		else:
			set_password = u''

		if self._source_login is not None:
			login_clause = '%s@' % (self._source_login,)
		else:
			login_clause = ''

		cmd = cmd_command(
			set_password +
			u'{rsync_bin} --no-perms -t -r {source_path} rsync://%s{source_ip}/{target_path}' % (
				login_clause,
			)
		)

		args = dict(
			rsync_bin=self._target_rsync_bin,
			source_ip=self._source_ip,
			source_path=source_path,
			target_path=target_path,
		)

		self._run_command(cmd, args)

	def list_files(self, source_path):
		if self._source_password is not None:
			set_password = u'set RSYNC_PASSWORD=%s&&' % (self._source_password,)
		else:
			set_password = u''

		if self._source_login is not None:
			login_clause = '%s@' % (self._source_login,)
		else:
			login_clause = ''

		cmd = cmd_command((
			u'{set_password}{rsync_binary_path} -r --list-only rsync://{login_clause}{source_ip}/{source_path}').format(
			set_password=set_password,
			rsync_binary_path=self._target_rsync_bin,
			login_clause=login_clause,
			source_ip=self._source_ip,
			source_path=source_path,
		))

		with self._target.runner() as runner:
			exit_code, stdout, stderr = runner.sh_unchecked(cmd)

		if exit_code != 0:
			raise RsyncClientNonZeroExitCode(
				exit_code, stdout, stderr, cmd
			)
		return stdout

	def _run_command(self, cmd, args):
		for attempt in range(0, self._max_attempts):
			with self._target.runner() as runner:
				exit_code, stdout, stderr = runner.sh_unchecked(cmd, args)

			if exit_code == 0:
				if attempt > 0:
					logger.info(
						messages.RSYNC_INTERACTION_OK_AFTER_ATTEMPTS.format(
							source_ip=self._source_ip, target_ip=self._target.ip(), attempts=attempt + 1
						)
					)
				return stdout
			else:
				if attempt >= self._max_attempts - 1:
					raise RsyncClientNonZeroExitCode(
						exit_code, stdout, stderr, cmd.format(**args)
					)
				else:
					is_restart = False
					message = messages.RSYNC_INTERACTION_FAILED
					if self._server is not None and attempt >= self._attempts_before_restart - 1:
						message += messages.TRY_RESTART_SERVER_AND_RETRY
						is_restart = True
					else:
						message += ', ' + messages.RSYNC_RETRY_IN
					logger.error(
						message.format(
							source_ip=self._source_ip,
							target_ip=self._target.ip(),
							interval=self._interval_between_attempts
						)
					)
					if is_restart:
						self._server.restart()
					else:
						sleep(self._interval_between_attempts, messages.SLEEP_RETRY_RSYNC)


class RsyncClientNonZeroExitCode(MigrationError):
	def __init__(self, exit_code, stdout, stderr, command):
		self.exit_code = exit_code
		self.stdout = stdout
		self.stderr = stderr
		self.command = command
