import logging
import ntpath
from collections import namedtuple

from parallels.common import MigrationError
from parallels.common.utils import windows_utils
from parallels.common.utils.steps_profiler import sleep
from parallels.common.utils import windows_thirdparty
from parallels.common.utils.windows_utils import cmd_command
from parallels.common.utils.windows_utils import file_exists

logger = logging.getLogger(__name__)

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


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:
			if file_exists(runner, ntpath.join(base_path, 'rsync.exe')):
				logger.debug(
					u"Rsync is already installed on server, skip installation."
					u"If you want to reinstall it, stop it with stop.bat script, "
					u"remove '%s' directory and then restart migration", 
					self.server.get_session_file_path('rsync')
				)
			else:
				installer_path = self.server.get_session_file_path(
					'rsync-installer.exe'
				)
				runner.upload_file(windows_thirdparty.get_rsync_installer_bin(), installer_path)
				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:
			"""Configure rsync server (rsyncd.conf and rsyncd.secrets)"""
			runner.sh(cmd_command(
				r'cd "{rsync_dir}"&&configure.bat '
				r'{source_servers} {vhosts_dir} {migrator_dir} '
				r'{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:
			"""Stop rsync if it is running, then start"""
			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('Unable to set vhosts_dir: rsync server at %s is not configured yet' % 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

	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} "
			u"--no-perms -t -r %s"
			u"rsync://%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} "
			u"--no-perms -t -r "
			u"{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(
						'Interaction via rsync with server at {source_ip} ad client at {target_ip} was finished '
						'successfully after {attempts} attempt(s).'.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 = 'Interaction via rsync with server at {source_ip} and client at {target_ip} was failed'
					if self._server is not None and attempt >= self._attempts_before_restart - 1:
						message += ', try to restart server and retry'
						is_restart = True
					else:
						message += ', retry in {interval} seconds'
					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, 'Retry interaction via 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