#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import argparse
import configparser
import os
import sys
import json
import requests

import django

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dnmgmt.settings")
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(BASE_DIR)
django.setup()

from django.db.models import Count
from domains.models import Domain, ReverseZone


__VERSION__ = '0.1'


def _parser():
	parser = argparse.ArgumentParser(
	             #prog='foo',
	             #description='do some awesome foo',
	)

	parser.add_argument("--pdns-host", default="127.0.0.1", help="PDNS host")
	parser.add_argument("--pdns-port", default=8081, help="PDNS port")
	parser.add_argument("-c", "--config", default=None, type=argparse.FileType("r"), help="Path to config file (default path: ./dns-sync.conf, /etc/dns-sync.conf)")
	parser.add_argument("--api-key", default=None, help="PDNS API key")
	parser.add_argument("--version", action="version", version="%(prog)s " + __VERSION__)

	return parser

def mergeDomains(zoneData, pdnsData):
	rrAdd = []
	rrData = pdnsData['rrsets']

	for domain, rrType, records in zoneData:
		found = False
		pdnsDom = list(filter(lambda _x: _x['name'] == domain and _x['type'] == rrType, rrData))
		if len(pdnsDom) > 0:
			rrSet = set(_x['content'] for _x in pdnsDom[0]['records'])
			if rrSet == set(records):
				found = True

		if not found:
			# new domain!
			rrAdd.append({
				"name": domain,
				"type": rrType,
				"ttl": 60*60,
				"changetype": "REPLACE",
				"records": [{"content": record, "disabled": False} for record in records],
			})

	return rrAdd


def removeOldDomains(zoneData, pdnsData):
	rrDel = []

	#print("zone data", zoneData)
	#print("pdnsData", pdnsData)
	for entry in pdnsData['rrsets']:
		# search for name/type in domain dict. if non-existtant mark for deletion
		# this could be much more efficient with a dict! name: [rrset...]
		if not any(entry['name'] == _x[0] and entry['type'] == _x[1] for _x in zoneData):
			rrDel.append({
				"changetype": "DELETE",
				"name": entry["name"],
				"type": entry["type"],
			})

	return rrDel

def handleNameserver(ns, servers, usedNameservers, domains):
	servers.append(ns.name)

	if ns.name not in usedNameservers and (ns.glueIPv4 or ns.glueIPv6):
		usedNameservers.append(ns.name)
		if ns.glueIPv4:
			domains.append((ns.name, "A", [ns.glueIPv4]))
		if ns.glueIPv6:
			domains.append((ns.name, "AAAA", [ns.glueIPv6]))


def getDomainsFromQueryset(qs, zone, glueRecords, usedNameservers, v4reverse=False):
	for domain in qs:
		servers = []
		for ns in domain.nameservers.all():
			servers.append(ns.name)

			if ns.name not in usedNameservers and (ns.glueIPv4 or ns.glueIPv6):
				usedNameservers.append(ns.name)
				if ns.glueIPv4:
					glueRecords.append((ns.name, "A", [ns.glueIPv4]))
				if ns.glueIPv6:
					glueRecords.append((ns.name, "AAAA", [ns.glueIPv6]))

		zone.append((domain.getZone(), "NS", servers))

		if v4reverse:
			# for ipv4 reverse we have to do some extra work in case of classless delegations
			# see RFC2317
			net = domain.parentNet.getNetwork()
			if net.prefixlen % 8 != 0:
				revZone = domain.getZone()
				parts = str(net.network_address).split(".")
				baseZone = ".".join(reversed(parts[:net.prefixlen // 8])) + ".in-addr.arpa."
				startNo = int(parts[net.prefixlen // 8])
				lenExp = 8 - (net.prefixlen % 8)

				for i in range(2 ** lenExp):
					no = startNo + i
					zone.append(("%d.%s" % (no, baseZone), "CNAME", ["%d.%s" % (no, revZone)]))

def mergeDomainsWithPdns(s, args, zone, zoneData, protectedRecords=[]):
	url = "http://%s:%s/api/v1/servers/localhost/zones/%s" % (args.pdns_host, args.pdns_port, zone,)
	pdnsData = s.get(url).json()

	baseProtectedRecords = [
		(zone, "NS", []),
		(zone, "SOA", []),
	]

	# add dn. (NS + glue Nameservers)
	newDomains = mergeDomains(zoneData, pdnsData)
	print("Add/replace", newDomains)

	if len(newDomains) > 0:
		r = s.patch(url, data=json.dumps({'rrsets': newDomains}))
		if r.status_code != 204:
			raise RuntimeError("Could not update records in powerdns, API returned %d" % r.status_code)

	delDomains = removeOldDomains(zoneData + protectedRecords + baseProtectedRecords, pdnsData)
	print("Del", delDomains)
	r = s.patch(url, data=json.dumps({'rrsets': delDomains}))
	if r.status_code != 204:
		raise RuntimeError("Could not update records in powerdns, API returned %d" % r.status_code)


def main():
	parser = _parser()
	args = parser.parse_args()

	config = configparser.ConfigParser()
	if args.config:
		config.read_file(args.config)
	else:
		config.read([os.path.join(os.path.abspath(__file__), "dns-sync.conf"), "/etc/dns-sync.conf"])
		#print(config)
		#print(config.get("DEFAULT", "api-key"))
		#print(config.has_section("DEFAULT"), config.has_option("DEFAULT", "api-key"))

	if args.api_key:
		config["DEFAULT"]["api-key"] = args.api_key

	if not config.has_option("DEFAULT", "api-key"):
		print("Error: Could not find api-key (not present in config under [DEFAULT]; not given via command line)", file=sys.stderr)
		sys.exit(2)

	s = requests.Session()
	s.headers = {
		'X-API-Key': config.get("DEFAULT", "api-key"),
		'Accept': 'application/json'
	}

	domains = []
	rzone4 = []
	rzone6 = []
	usedNameservers = []

	# assenble domain data
	# dn.
	qs = Domain.objects.annotate(nsCount=Count('nameservers')).filter(nsCount__gt=0).order_by("name")
	getDomainsFromQueryset(qs, domains, domains, usedNameservers)

	# reverse zones
	qs = ReverseZone.objects.annotate(nsCount=Count('nameservers')).filter(nsCount__gt=0).order_by("parentNet__address")
	qs4 = filter(lambda _revz: _revz.getNetwork().version == 4, qs)
	qs6 = filter(lambda _revz: _revz.getNetwork().version == 6, qs)
	getDomainsFromQueryset(qs4, rzone4, domains, usedNameservers, v4reverse=True)
	getDomainsFromQueryset(qs6, rzone6, domains, usedNameservers)

	#print("dn.", domains)
	#print("v4", rzone4)
	#print("v6", rzone6)

	mergeDomainsWithPdns(s, args, "dn.", domains)
	mergeDomainsWithPdns(s, args, "10.in-addr.arpa.", rzone4)
	mergeDomainsWithPdns(s, args, "3.2.7.c.3.a.d.f.ip6.arpa.", rzone6)



if __name__ == '__main__':
	main()