from parallels.core import messages
import logging
import itertools
import re
from collections import namedtuple
from parallels.core.utils.common import split_string_by_whitespace_chars

from parallels.core.utils.common.ip import is_ipv4, is_ipv6

Rec = namedtuple('Rec', ('rec_type', 'src', 'dst', 'opt'))

logger = logging.getLogger(__name__)


def replace_resource_record_ips(subscription):
	"""Replace source panel IPs with target IPs in subscription DNS zone data.
	
	Assuming, that each source subscription has a single assigned IP address of
	each type (IPv4/IPv6) used by all its services, replace that IP with the
	new ones of the target server.
	"""
	target_ips = []
	if subscription.target_public_web_ipv4 is not None:
		target_ips.append(subscription.target_public_web_ipv4)
	if subscription.target_public_web_ipv6 is not None:
		target_ips.append(subscription.target_public_web_ipv6)

	if len(target_ips) == 0:
		# Subscription has no web hosting, try to take IP of mail hosting.
		# In most cases that should work fine.
		if subscription.target_public_mail_ipv4 is not None:
			target_ips.append(subscription.target_public_mail_ipv4)

	if len(target_ips) == 0:
		logger.error(
			messages.SUBSCRIPTION_S_NO_IP_ADDRESSES_TARGET % (subscription.name)
		)
		return

	source_ips = [subscription.converted_dump.ip, subscription.converted_dump.ipv6]

	mailsystem_ip_map = _get_mailsystem_ip_mapping(subscription)
	for domain in itertools.chain(
		subscription.converted_dump.iter_domains(),
		subscription.converted_dump.iter_aliases()
	):
		_replace_mailsystem_ips(domain, mailsystem_ip_map)
		if domain.dns_zone is None:
			continue

		dns_zone = UniqueDnsZoneAdapter(domain.dns_zone)
		for rec in list(domain.dns_zone.iter_dns_records()):
			if rec.dst in source_ips:
				dns_zone.remove_dns_record(rec)
				for new_ip in target_ips:
					rec_type = rec.rec_type
					if rec_type in ('A', 'AAAA'):
						if is_ipv4(new_ip):
							rec_type = 'A'
						elif is_ipv6(new_ip):
							rec_type = 'AAAA'

					dns_zone.add_dns_record(Rec(
						rec_type=rec_type, src=rec.src, 
						dst=new_ip, opt=rec.opt
					))
					logger.debug(messages.LOG_REPLACED_IP, rec, new_ip)
			elif rec.rec_type == 'TXT':
				_replace_spf_record_ips(dns_zone, rec, subscription)


def _replace_spf_record_ips(dns_zone, rec, subscription):
	"""Replace IP addresses in SPF records"""

	parts = split_string_by_whitespace_chars(rec.dst)
	parts_stripped = split_string_by_whitespace_chars(rec.dst.strip())
	new_parts = []
	if len(parts_stripped) > 0 and parts_stripped[0].lower() == 'v=spf1':
		for part in parts:
			if (
				subscription.converted_dump.ip is not None and
				_get_subscription_ipv4(subscription) is not None and
				part.lower() == 'ip4:%s' % subscription.converted_dump.ip
			):
				new_parts.append('ip4:%s' % _get_subscription_ipv4(subscription))
			elif (
				subscription.converted_dump.ip is not None and
				_get_subscription_ipv4(subscription) is not None and
				part[1:].lower() == 'ip4:%s' % subscription.converted_dump.ip
			):
				new_parts.append('%sip4:%s' % (part[0], _get_subscription_ipv4(subscription)))
			elif (
				subscription.converted_dump.ipv6 is not None and
				_get_subscription_ipv6(subscription) is not None and
				part.lower() == 'ip6:%s' % subscription.converted_dump.ipv6
			):
				new_parts.append('ip6:%s' % _get_subscription_ipv6(subscription))
			elif (
				subscription.converted_dump.ipv6 is not None and
				_get_subscription_ipv6(subscription) is not None and
				part[1:].lower() == 'ip6:%s' % subscription.converted_dump.ipv6
			):
				new_parts.append('%sip6:%s' % (part[0], _get_subscription_ipv6(subscription)))
			else:
				new_parts.append(part)

		dns_zone.remove_dns_record(rec)
		changed_record = Rec(rec_type=rec.rec_type, src=rec.src, dst=''.join(new_parts), opt=rec.opt)
		dns_zone.add_dns_record(changed_record)


def _replace_mailsystem_ips(domain, mailsystem_ip_map):
	"""Replace mailsystem source IPs with the target ones."""
	source_mail_ip_list = domain.node.findall('mailsystem/properties/ip/ip-address')
	for source_ip in source_mail_ip_list:
		if source_ip.text in mailsystem_ip_map:
			source_ip.text = mailsystem_ip_map[source_ip.text]


def _get_mailsystem_ip_mapping(subscription):
	"""Prepare a mapping from souce mail system IPs to target IPs."""
	mailsystem_ip_map = {}
	if subscription.source_mail_ip and subscription.target_mail_ip:
		mailsystem_ip_map[subscription.source_mail_ip] = subscription.target_mail_ip
	if subscription.source_mail_ipv6 and subscription.target_mail_ipv6:
		mailsystem_ip_map[subscription.source_mail_ipv6] = subscription.target_mail_ipv6
	return mailsystem_ip_map


class UniqueDnsZoneAdapter(object):
	"""DNS zone adapter which makes changed DNS records unique.

	This class makes resulting DNS records unique: skip duplicates. Example:
	when there are 2 source web IPs, but only one target web IP. 
	"""
	def __init__(self, dns_zone):
		self.dns_zone = dns_zone

		self.xml_recs = {
			self._create_rec(r): r
			for r in dns_zone.iter_dns_records()
		}

	def remove_dns_record(self, rec):
		rec = self._create_rec(rec)
		self.dns_zone.remove_dns_record(self.xml_recs[rec])
		del self.xml_recs[rec]

	def add_dns_record(self, rec):
		rec = self._create_rec(rec)
		if rec not in self.xml_recs:
			self.xml_recs[rec] = self.dns_zone.add_dns_record(rec)
		else:
			pass

	@staticmethod
	def _create_rec(xml_rec):
		return Rec(
			rec_type=xml_rec.rec_type, src=xml_rec.src, 
			dst=xml_rec.dst, opt=xml_rec.opt
		)


def render_record_value(value, vars):
	"""Expand DNS template record value using given parameter values.

	DNS record value looks like "mail.<domain>." or "<ipv6.mail>".
	Plesk omits records for which referenced parameter wasn't found.

	vars: parameters dictionary.

	Returns: string or None if any referenced value isn't found
	in parameters or it's value is None.
	"""
	class NoVar(BaseException):
		pass

	try:
		def expand(match):
			v = vars.get(match.group(1))
			if v is None:
				raise NoVar()
			return v
		return re.sub(r'<([\w.]+)>', expand, value)
	except NoVar:
		return None


def render_dns_record_template(record, values):
	src = render_record_value(record.src, values)
	dst = render_record_value(record.dst, values)
	if src is None or dst is None:
		return None
	else:
		return Rec(record.rec_type, src, dst, record.opt)


def render_dns_zone_template(dns_zone_template, variables):
	"""Instantiate DNS zone template with given parameters.

	dns_zone_template: sequence of Rec
	variables: dictionary

	Returns dictionary where keys are instantiated DNS records and
	values are corresponding template records.

	If DNS template record can't be expanded (for example, because
	it refers unknown parameter) it is silently skipped.
	"""
	zone = {}
	for tmpl_rec in dns_zone_template:
		rec = render_dns_record_template(tmpl_rec, variables)
		if rec is not None:
			zone[rec] = tmpl_rec

	return zone


def extract_custom_records(dns_zone_template, variables, records):
	"""Partition records into standard and custom ones.

	Returns pair:
	# first element is dictionary with records as keys and record templates as values
	# second is list of custom DNS records
	"""
	# Instantiate DNS zone template with given parameters.
	standard_records = render_dns_zone_template(dns_zone_template, variables)
	
	template_recs = {}
	custom_recs = []

	# Now enumerate all existing records and check whether it
	# could be generated from template or not.
	for rec in records:
		if rec in standard_records:
			template_recs[rec] = standard_records[rec]
		else:
			custom_recs.append(rec)

	return template_recs, custom_recs


def pretty_record_str(rec):
	opt_str = u" %s" % (rec.opt,) if rec.opt is not None and rec.opt != "" else ""
	return u"%s %s%s %s" % (rec.src, rec.rec_type, opt_str, rec.dst)


def _get_subscription_ipv4(subscription):
	if subscription.target_public_web_ipv4 is not None:
		return subscription.target_public_web_ipv4
	else:
		# Subscription has no web hosting, try to take IP of mail hosting.
		# In most cases that should work fine.
		return subscription.target_public_mail_ipv4


def _get_subscription_ipv6(subscription):
	if subscription.target_public_web_ipv6 is not None:
		return subscription.target_public_web_ipv6
	elif subscription.target_public_web_ipv4 is None:
		# Subscription has no web hosting, try to take IP of mail hosting.
		# In most cases that should work fine.
		return subscription.target_public_mail_ipv6
	else:
		# Subscription has web hosting, but has no IPv6 - return None
		return None