# -*- coding: utf-8 -*-
#
#  This file is part of django-powerdns-manager.
#
#  django-powerdns-manager is a web based PowerDNS administration panel.
#
#  Development Web Site:
#    - http://www.codetrax.org/projects/django-powerdns-manager
#  Public Source Code Repository:
#    - https://source.codetrax.org/hgroot/django-powerdns-manager
#
#  Copyright 2012-2016 George Notaras <gnot@g-loaded.eu>
#
#  Licensed under the Apache License, Version 2.0 (the "License");
#  you may not use this file except in compliance with the License.
#  You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
#  Unless required by applicable law or agreed to in writing, software
#  distributed under the License is distributed on an "AS IS" BASIS,
#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#  See the License for the specific language governing permissions and
#  limitations under the License.
#

import datetime
import time
import hashlib
import base64
import string
import StringIO
import re

import dns.zone
import dns.query
from dns.zone import BadZone, NoSOA, NoNS, UnknownOrigin
from dns.exception import DNSException
import dns.rdataclass
import dns.rdatatype
import dns.rdtypes
import dns.rdtypes.ANY
from dns.rdtypes.ANY import *
import dns.rdtypes.IN
from dns.rdtypes.IN import *
from dns.name import Name

try:
    from django.apps import apps
    get_model = apps.get_model
except ImportError:
    from django.db.models.loading import cache
    get_model = cache.get_model
from django.utils.crypto import get_random_string
from django.core.exceptions import ValidationError
from django.core.validators import validate_ipv46_address

from powerdns_manager import settings



def validate_hostname(hostname,
        reject_ip=True, supports_cidr_notation=False, supports_wildcard=False):
    """Validates that ``hostname`` does not contain illegal characters.
    
    In case ``hostname`` is not validated a ``ValidationError`` is raised.
    
    PowerDNS expects to find FQDN hostnames, so if the hostname ends with a dot,
    it is rejected.
    
    By default, the valid hostname characters are:
    
        A-Za-z0-9._-
    
    The pattern above allows the hostname to be an IP address (IPv4). By default,
    if hostname validates as an IP address, it is rejected. To allow IP addresses
    as hostnames, set ``reject_ip`` to False when calling this validator.
    
    If ``supports_cidr_notation`` is True, then ``/`` is allowed too.
    
    If ``supports_wildcard`` is True, then ``*`` is allowed too, unless
    the ``PDNS_ALLOW_WILDCARD_NAMES`` has been set to False (default is True).
    
    """
    if not hostname:
        return
    
    if hostname.endswith('.'):
        raise ValidationError('PowerDNS expects to find FQDN hostnames without trailing dot')
    
    if reject_ip:
        # Check if hostname is an IP and reject it if it is.
        try:
            validate_ipv46_address(hostname)
        except ValidationError:
            pass
        else:
            raise ValidationError('IP addresses cannot be used as hostnames')
    
    valid_sequence = 'A-Za-z0-9._\-'    # dash needs to be escaped
    
    if supports_cidr_notation:
        valid_sequence = '%s/' % valid_sequence
        
    if supports_wildcard:
        if settings.PDNS_ALLOW_WILDCARD_NAMES:
            valid_sequence = '%s*' % valid_sequence
    
    if not re.match('^[%s]+$' % valid_sequence, hostname):
        raise ValidationError('The hostname contains illegal characters')


def interchange_domain(data, domain1, domain2):
    """Replaces domain1 with domain2 in data.
    
    data: RR's name or content
    
    Replacement occurs only in the base domain.
    
    TODO: improve this description.
    
    """
    if not data:    # cacn happen with content of empty non-terminals
        return None
    elif len(domain1) > len(data):
        return data
    elif not data.endswith(domain1):
        return data
    elif data == domain1:
        return domain2
    
    data_parts = data.split('.')
    domain1_parts = domain1.split('.')
    
    new_data_parts = data_parts[:-len(domain1_parts)]
    return '%s.%s' % ('.'.join(new_data_parts), domain2) 


def generate_serial(serial_old=None):
    """Return a serial number for the zone in the form YYYYMMDDNN.
    
    Currently returns the Unix timestamp.
    
    """
    curdate = datetime.date.today()
    
    if serial_old is not None:
        # Split provided serial into YYYY, MM, DD, and NN.
        yyyy = serial_old[0:4]
        mm = serial_old[4:6]
        dd = serial_old[6:8]
        nn = serial_old[8:10]
        
        # Try to get a datetime object of the old serial's date.
        try:
            serial_date = datetime.date(year=int(yyyy), month=int(mm), day=int(dd))
        except ValueError:
            # The serial is corrupted. Pass. A new serial will be generated.
            pass
        else:
            # If the old serial number's date is equal to the current date, then
            # increment nn by one and return the new serial.
            if serial_date == curdate:
                if int(nn) == 99:
                    # If you make more than 99 zone updates within a single day,
                    # DNS is not for you!! NN stays at 99.
                    nn_new = 99
                else:
                    nn_new = str(int(nn) + 1).zfill(2)[:2]  # Is always 2 digits long
                return '%s%s%s%s' % (yyyy, mm, dd, nn_new)
    
    # A new serial needs to be generated for the current date.
    return '%s%s%s%s' % (
        curdate.year, str(curdate.month).zfill(2), str(curdate.day).zfill(2), '01')


def generate_serial_timestamp(old_serial=None, is_timestamp=True):
    """Return the current Unix timestamp."""
    return int(time.time())


def generate_api_key():
    return get_random_string(
        length=24, allowed_chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789')


def clean_zonefile(data):
    EOL = '\n'
    lines = []
    f = StringIO.StringIO(data)
    for line in f:
        line = line.strip()
        if line:
            lines.append(line)
    f.close()
    return EOL.join(lines)


def process_zone_file(origin, zonetext, owner, overwrite=False):
    """Imports zone to the database.
    
    No checks for existence are performed in this file. For form processing,
    see the ``import_zone_view`` view.
    

    
    """
    if origin:
        origin = Name((origin.rstrip('.') + '.').split('.'))
    else:
        origin = None
    
    zonetext = clean_zonefile(str(zonetext))
    
    try:
        zone = dns.zone.from_text(zonetext, origin=origin, relativize=False)
        if not str(zone.origin).rstrip('.'):
            raise UnknownOrigin
        
        process_and_import_zone_data(zone, owner, overwrite)
        
    except NoSOA:
        raise Exception('The zone has no SOA RR at its origin')
    except NoNS:
        raise Exception('The zone has no NS RRset at its origin')
    except UnknownOrigin:
        raise Exception('The zone\'s origin is unknown')
    except BadZone:
        raise Exception('The zone is malformed')
    except DNSException, e:
        #raise Exception(str(e))
        raise Exception('The zone is malformed')


def process_axfr_response(origin, nameserver, owner, overwrite=False):
    """
    origin: string domain name
    nameserver: IP of the DNS server
    
    """
    origin = Name((origin.rstrip('.') + '.').split('.'))
    axfr_query = dns.query.xfr(nameserver, origin, timeout=5, relativize=False, lifetime=10)

    try:
        zone = dns.zone.from_xfr(axfr_query, relativize=False)
        if not str(zone.origin).rstrip('.'):
            raise UnknownOrigin
        
        process_and_import_zone_data(zone, owner, overwrite)

    except NoSOA:
        raise Exception('The zone has no SOA RR at its origin')
    except NoNS:
        raise Exception('The zone has no NS RRset at its origin')
    except UnknownOrigin:
        raise Exception('The zone\'s origin is unknown')
    except BadZone:
        raise Exception('The zone is malformed')
    except DNSException, e:
        if not str(e):
            raise Exception('Transfer Failed')
        raise Exception(str(e))
    

def process_and_import_zone_data(zone, owner, overwrite=False):
    """
    zone: dns.zone
    
    *****
    Special kudos to Grig Gheorghiu for demonstrating how to manage zone files
    using dnspython in the following article:
    http://agiletesting.blogspot.com/2005/08/managing-dns-zone-files-with-dnspython.html
    *****
    
    """
    Domain = get_model('powerdns_manager', 'Domain')
    Record = get_model('powerdns_manager', 'Record')
    
    # Check if zone already exists in the database.
    try:
        domain_instance = Domain.objects.get(name=str(zone.origin).rstrip('.'))
    except Domain.DoesNotExist:
        pass    # proceed with importing the new zone data
    else:   # Zone exists
        if overwrite:
            # If ``overwrite`` has been checked, then delete the current zone.
            domain_instance.delete()
        else:
            raise Exception('Zone already exists. Consider using the "overwrite" option')
    
    # Import the new zone data to the database.
    
    # Create a domain instance
    
    #the_domain = Domain.objects.create(name=str(zone.origin).rstrip('.'), type='NATIVE', master='')
    the_domain = Domain(
        name=str(zone.origin).rstrip('.'),
        type='NATIVE',
        master='')
    
    if owner is not None:
        the_domain.created_by = owner
    
    the_domain.save()
    
    # Create RRs
    for name, node in zone.nodes.items():
        rdatasets = node.rdatasets
        
        for rdataset in rdatasets:
            
            # Check instance variables of types:
            # http://www.dnspython.org/docs/1.10.0/html/dns.rdtypes-module.html

            for rdata in rdataset:
                
                rr = Record(
                    domain=the_domain,
                    name=str(name).rstrip('.'), # name is the dnspython node name
                    change_date=int(time.time()),
                    ttl = rdataset.ttl
                )
                
                if rdataset.rdtype == dns.rdatatype._by_text['SOA']:
                    # Set type
                    rr.type = 'SOA'
                    # Construct content
                    rr.content = '%s %s %s %s %s %s %s' % (
                        str(rdata.mname).rstrip('.'),
                        str(rdata.rname).rstrip('.'),
                        rdata.serial,
                        rdata.refresh,
                        rdata.retry,
                        rdata.expire,
                        rdata.minimum
                    )

                elif rdataset.rdtype == dns.rdatatype._by_text['NS']:
                    # http://www.dnspython.org/docs/1.10.0/html/dns.rdtypes.ANY.NS.NS-class.html
                    rr.type = 'NS'
                    rr.content = str(rdata.target).rstrip('.')

                elif rdataset.rdtype == dns.rdatatype._by_text['MX']:
                    # http://www.dnspython.org/docs/1.10.0/html/dns.rdtypes.ANY.MX.MX-class.html
                    rr.type = 'MX'
                    rr.content = str(rdata.exchange).rstrip('.')
                    rr.prio = rdata.preference
                
                elif rdataset.rdtype == dns.rdatatype._by_text['TXT']:
                    # http://www.dnspython.org/docs/1.10.0/html/dns.rdtypes.ANY.TXT.TXT-class.html
                    rr.type = 'TXT'
                    rr.content = '"%s"' % ''.join(rdata.strings)
                
                elif rdataset.rdtype == dns.rdatatype._by_text['CNAME']:
                    # http://www.dnspython.org/docs/1.10.0/html/dns.rdtypes.ANY.CNAME.CNAME-class.html
                    rr.type = 'CNAME'
                    rr.content = str(rdata.target).rstrip('.')
                    
                elif rdataset.rdtype == dns.rdatatype._by_text['A']:
                    # http://www.dnspython.org/docs/1.10.0/html/dns.rdtypes.IN.A.A-class.html
                    rr.type = 'A'
                    rr.content = rdata.address
                
                elif rdataset.rdtype == dns.rdatatype._by_text['AAAA']:
                    # http://www.dnspython.org/docs/1.10.0/html/dns.rdtypes.IN.AAAA.AAAA-class.html
                    rr.type = 'AAAA'
                    rr.content = rdata.address
                
                elif rdataset.rdtype == dns.rdatatype._by_text['SPF']:
                    # http://www.dnspython.org/docs/1.10.0/html/dns.rdtypes.ANY.SPF.SPF-class.html
                    rr.type = 'SPF'
                    rr.content = '"%s"' % ''.join(rdata.strings)
                
                elif rdataset.rdtype == dns.rdatatype._by_text['PTR']:
                    # http://www.dnspython.org/docs/1.10.0/html/dns.rdtypes.ANY.PTR.PTR-class.html
                    rr.type = 'PTR'
                    rr.content = str(rdata.target).rstrip('.')
                
                elif rdataset.rdtype == dns.rdatatype._by_text['SRV']:
                    # http://www.dnspython.org/docs/1.10.0/html/dns.rdtypes.IN.SRV.SRV-class.html
                    rr.type = 'SRV'
                    rr.content = '%d %d %s' % (rdata.weight, rdata.port, str(rdata.target).rstrip('.'))