import errno
import re

from parallels.common.utils import plesk_utils
from parallels.utils import obj


# TODO for Expand implementation, move this class to a module in common

#---------------------------------------- public only for Plesk migrator
DnsServiceType = obj(BIND_LINUX = 'bind-l', BIND_WINDOWS = 'bind-w', MSDNS = 'msdns')

def getPhysicalDnsForwardingImpl(service_type, logger, migrator_server, plesk_id, runner):
	"""Return an object of appropriate class implementing DNS forwarding for that service.
	"""
	if service_type == DnsServiceType.MSDNS:
		return MsDnsForwarding(logger, migrator_server, plesk_id, runner)
	elif service_type == DnsServiceType.BIND_WINDOWS:
		return WindowsBindForwarding(logger, migrator_server, plesk_id, runner)
	elif service_type == DnsServiceType.BIND_LINUX:
		return LinuxBindForwarding(logger, migrator_server, plesk_id, runner)
	else:
		assert False, u"Migrator is trying to get the forwarding implementation for an unknown DNS service type: '%s'" % service_type


#---------------------------------------- public only for migrators implementing their own DNS forwarding with this interface --------
class DnsForwardingBase(object):
	"""This class defines only the common initialization,
	   And declares two public functions that should be defined separately for different services.
	"""
	def __init__(self, logger, migrator_server, plesk_id, runner):
		self._logger = logger
		self._migrator_server = migrator_server
		self._plesk_id = plesk_id
		self._runner = runner

	def add_slave_subdomain_zones(self, subdomains_masters):
		raise NotImplementedError()

	def remove_slave_subdomain_zones(self, subdomains):
		raise NotImplementedError()

#---------------------------------------- this file's private code follows --------------------------

class MsDnsForwarding(DnsForwardingBase):
	"""This class implements the 2 public functions of DnsForwardingBase, for Microsoft DNS
	"""
	def __init__(self, logger, migrator_server, plesk_id, runner):
		super(MsDnsForwarding, self).__init__(logger, migrator_server, plesk_id, runner)
		self._plesk_dir = plesk_utils.get_windows_plesk_dir(runner)

	def add_slave_subdomain_zones(self, subdomains_masters):
		def add(name, master):
			"""Add zone in slave mode, with specified master server
			   Return error string (None if there is no error, actual string if there is a error)
			"""
			rc, stdout, stderr = self._runner.run_unchecked(ur"%s\admin\bin\dnscmd" % self._plesk_dir, ['/ZoneAdd', name.encode('idna'), '/Secondary', master])
			if rc == 0:
				return None
			# Expected error that we're going to ignore, as it will occur on when we set up DNS forwarding second time and further:
			# Command failed:  DNS_ERROR_ZONE_ALREADY_EXISTS     9609  (00002589)
			# return code is 137, which may be 9609 % 256, but may not. so we check the code in stdout as well
			if rc == 137 and "9609" in stdout:
				return None
			return stdout + stderr

		err_zones = {}
		for name, master in subdomains_masters.iteritems():
			error = add(name, master)
			if error is not None:
				errmsg = u"Could not add the zone '%s' (secondary, with master server '%s') to DNS. The error is: '%s'" % (name, master, error)
				self._logger.error(errmsg)
				err_zones[name] = errmsg
		return err_zones


	def remove_slave_subdomain_zones(self, subdomains):
		def remove(name):
			"""Remove zone from DNS
			   Return error string (None if there is no error, actual string if there is a error)
			"""
			rc, stdout, stderr = self._runner.run_unchecked(ur"%s\admin\bin\dnscmd" % self._plesk_dir, ['/ZoneDelete', name.encode('idna'), '/f'])
			if rc == 0:
				return None
			# Expected error that we're going to ignore, as it will occur on when we undo DNS forwarding second time and further:
			# Command failed:  DNS_ERROR_ZONE_DOES_NOT_EXIST     9601  (00002581)
			# return code is 129, which may be 9601 % 256, but may not. so we check the code in stdout as well
			if rc == 129 and "9601" in stdout:
				return None
			return stdout + stderr

		err_zones = {}
		for name in subdomains:
			error = remove(name)
			if error is not None:
				errmsg = u"Could not remove the zone '%s' from DNS. The error is: %s" % (name, error)
				self._logger.error(errmsg)
				err_zones[name] = errmsg
		return err_zones

class BindIndexEditor(object):
	"""This class implements editing the BIND zones index file: adding or removing zones from there.
	   It either fails (with exception) or succeeds for all specified zones, so does not return the set of failed zones.
	"""
	def __init__(self, index_filename, prefix, newline):
		self._index_filename = index_filename
		self._prefix = prefix
		self._newline = newline

	def add_zones_to_index(self, zones_masters):
		zones_index = self._read_index()
		new_index = [line for line in zones_index if not self._line_refers_to_one_of_zones(line, zones_masters.keys()) and not len(line) == 0]
		for zone, master in zones_masters.iteritems():
			new_index.append(ur"""zone "{zone}" {{ type slave; file "{prefix}{zone}"; masters {{ {master}; }}; }};""".format(prefix=self._prefix, zone=zone.encode('idna'), master=master))
		self._write_index(new_index)

	def remove_zones_from_index(self, zones):
		zones_index = self._read_index()
		new_index = [line for line in zones_index if not self._line_refers_to_one_of_zones(line, zones) and not len(line) == 0]
		self._write_index(new_index)

	def _read_index(self):
		try:
			with open(self._index_filename, 'r') as zones_file:
				zones_index = zones_file.read().split(self._newline)
		except IOError as e:
			if e.errno == errno.ENOENT:
				zones_index = []
			else:
				raise
		return zones_index

	@staticmethod
	def _line_refers_to_one_of_zones(line, zones):
		for zone in zones:
			pattern = u"""^zone "%s" {.*};""" % zone.encode('idna')
			if re.match(pattern, line):
				return True
		return False

	def _write_index(self, zones_index):
		with open(self._index_filename, 'w') as zones_file:
			content = self._newline.join(zones_index)
			if len(content) > 0:
				content += self._newline     # add trailing newline, as .join doesn't
			zones_file.write(content)

class BindForwarding(DnsForwardingBase):
	"""This class implements the 2 public functions of DnsForwardingBase, for BIND
	   As such, it defines, what to do to add or remove subdomain zones, leaving unimplemented the HOW to do that
	"""
	def add_slave_subdomain_zones(self, subdomains_masters):
		try:
			self._download_slave_subdomains_index()	# different remote path, different exceptions for ENOENT
			self._add_subdomains_to_index(subdomains_masters) # different prefix and newline
			self._upload_slave_subdomains_index() # different remote path
			self._include_index_to_named_conf() # different remote path, include value and implementation how to add it
			self._restart_named() # different implementation
			return {} # operation did not fail - no failed subdomains
		except Exception as e:
			self._logger.debug(u'Exception:', exc_info=e)
			self._logger.error(u"Could not add slave DNS zones for subdomains, error: %s", e)
			return {
				zone: u"Could not add a slave DNS zone for subdomain '%s', error: '%s'" % (zone, e)
				for zone in subdomains_masters
			}

	def remove_slave_subdomain_zones(self, subdomains):
		try:
			self._download_slave_subdomains_index()
			self._remove_subdomains_from_index(subdomains)
			self._upload_slave_subdomains_index()
			self._restart_named()
			return {} # operation did not fail - no failed subdomains
		except Exception as e:
			self._logger.debug(u'Exception:', exc_info=e)
			self._logger.error(u"Could not remove subdomains' DNS zones, error: %s", e)
			return {
				zone: u"Could not remove the DNS zone of subdomain '%s', error: '%s'" % (zone, e)
				for zone in subdomains
			}

	def _download_slave_subdomains_index(self):
		raise NotImplementedError()

	def _add_subdomains_to_index(self, subdomains_masters):
		raise NotImplementedError()

	def _remove_subdomains_from_index(self, subdomains):
		raise NotImplementedError()

	def _upload_slave_subdomains_index(self):
		raise NotImplementedError()

	def _include_index_to_named_conf(self):
		raise NotImplementedError()

	def _restart_named(self):
		raise NotImplementedError()


class LinuxBindForwarding(BindForwarding):
	"""This class implements the 6 private functions of BindForwarding, doing the actual changes, for BIND on Linux
	"""
	def __init__(self, logger, migrator_server, plesk_id, runner):
		super(LinuxBindForwarding, self).__init__(logger, migrator_server, plesk_id, runner)
		self._index_filename_local = migrator_server.get_session_file_path(u"subdomain_zones.%s" % (plesk_id))
		self._index_filename_remote_relative = '/etc/subdomain_zones'
		self._named_run_root_d = plesk_utils.get_unix_conf_var(runner, 'NAMED_RUN_ROOT_D')
		self._index_filename_remote = u"%s%s" % (self._named_run_root_d, self._index_filename_remote_relative)
		self._index_editor = BindIndexEditor(self._index_filename_local, prefix='', newline='\n')

	def _download_slave_subdomains_index(self):
		try:
			self._runner.get_file(self._index_filename_remote, self._index_filename_local)
		except IOError as e:
			if e.errno == errno.ENOENT:
				self._logger.debug(u'(expected) Exception:', exc_info=e)
			else:
				raise

	def _add_subdomains_to_index(self, subdomains_masters):
		self._index_editor.add_zones_to_index(subdomains_masters)

	def _remove_subdomains_from_index(self, subdomains):
		self._index_editor.remove_zones_from_index(subdomains)

	def _upload_slave_subdomains_index(self):
		self._runner.upload_file(self._index_filename_local, self._index_filename_remote)

	def _include_index_to_named_conf(self):
		named_cf_remote = u"%s/etc/named.conf" % self._named_run_root_d
		named_cf_local = self._migrator_server.get_session_file_path(
			u"named.conf.%s" % (self._plesk_id,)
		)
		self._runner.get_file(named_cf_remote, named_cf_local)

		with open(named_cf_local, 'r') as f:
			named_cf_content = f.read()
		include_str = u"""include "%s";""" % self._index_filename_remote_relative
		if include_str not in named_cf_content:
			self._runner.sh(u"""(echo;echo '%s') >> "%s" """ % (include_str, named_cf_remote))

	def _restart_named(self):
		cmd = """
for startup_script in /etc/rc.d/named /etc/init.d/named /etc/init.d/bind9; do
	if [ -e $startup_script ]; then
		sh $startup_script restart
		exit $?
	fi
done
echo "Unable to restart BIND service - cannot find location of its startup script. Restart BIND service manually." 1>&2
exit 1"""
		self._runner.sh(cmd)

class WindowsBindForwarding(BindForwarding):
	"""This class implements the 6 private functions of BindForwarding, doing the actual changes, for BIND on Windows
	"""
	def __init__(self, logger, migrator_server, plesk_id, runner):
		super(WindowsBindForwarding, self).__init__(logger, migrator_server, plesk_id, runner)
		self._plesk_dir = plesk_utils.get_windows_plesk_dir(runner)	# needed in _include_index_to_named_conf
		self._index_filename_local = migrator_server.get_session_file_path(
			u"subdomain_zones.%s" % (plesk_id,)
		)
		self._index_filename_remote = ur"%s\dns\etc\subdomain_zones" % self._plesk_dir	# needed in _include_index_to_named_conf
		self._index_editor = BindIndexEditor(self._index_filename_local, prefix=ur"%s\dns\var\.slave." % self._plesk_dir, newline='\n\r')

	def _download_slave_subdomains_index(self):
		try:
			self._runner.get_file(self._index_filename_remote, self._index_filename_local)
		except Exception as e:
			if u"NT_STATUS_OBJECT_NAME_NOT_FOUND opening remote file" in str(e):
				self._logger.debug(u'(expected) Exception:', exc_info=e)
			else:
				raise

	def _add_subdomains_to_index(self, subdomains_masters):
		self._index_editor.add_zones_to_index(subdomains_masters)

	def _remove_subdomains_from_index(self, subdomains):
		self._index_editor.remove_zones_from_index(subdomains)

	def _upload_slave_subdomains_index(self):
		self._runner.upload_file(self._index_filename_local, self._index_filename_remote)

	def _include_index_to_named_conf(self):
		named_cf_remote = ur"%s\dns\etc\named.conf" % self._plesk_dir
		named_cf_local = self._migrator_server.get_session_file_path(
			u"named.conf.%s" % (self._plesk_id,)
		)
		self._runner.get_file(named_cf_remote, named_cf_local)

		with open(named_cf_local, 'r') as f:
			named_cf_content = f.read()
		include_str = u"""include "%s";""" % self._index_filename_remote
		if include_str not in named_cf_content:
			# echo: puts an empty line, & is cmd's commands separator
			self._runner.sh(u"""cmd /c "(echo:& echo %s) >> "%s"" """ % (include_str, named_cf_remote))

	def _restart_named(self):
		self._runner.sh_unchecked('net stop named')	# service named could be stopped - ignore this error. TODO check other errors
		self._runner.sh('net start named')

