import logging
import pathlib
import re
from typing import List, Tuple

import zonefilegen.core

# First line of each generated zone file should be a comment with the
# SHA-1 hex digest of the input toml file:
# ; Generated by zonefilegen, INPUT_SHA1: ea03443f2d9f8c580e73d2f8cd1016dc7174bddc
FIRST_LINE_PATTERN = re.compile(r'^\s*;.*INPUT_DIGEST:\s+(?P<digest>[0-9a-f]+)')
SOA_PATTERN = re.compile(r'^.+SOA[ \t]+(?P<mname>[\.\w]+)[ \t]+(?P<rname>[\.\w]+)[ \t]+\(\s*(?P<serial>[0-9]+)', re.MULTILINE)


def build_soa_record(soa_dict: dict, serial_number: int) -> zonefilegen.core.ResourceRecord:
    """
    Constructs a special SOA record with a specified serial number
    """
    soa_rec = zonefilegen.core.ResourceRecord()
    soa_rec.name = "@"
    soa_rec.ttl = None
    soa_rec.record_class = 'IN'
    soa_rec.record_type = 'SOA'
    soa_rec.data = f"{soa_dict['mname']} {soa_dict['rname']} ("\
        f"{serial_number} "\
        f"{soa_dict['refresh']} "\
        f"{soa_dict['retry']} " \
        f"{soa_dict['expire']} " \
        f"{soa_dict['negative_caching_ttl']})"
    return soa_rec


def build_reverse_zone(network,
                       ptr_candidates: Tuple,
                       default_ttl: int,
                       ns_records: List[Tuple[str, int, str]]) -> zonefilegen.core.Zone:
    """
    Checks a set of addresses if they are part of a network and
    include them as PTR records in a reverse zone for that network in such case.
    """
    rev_zone = zonefilegen.core.Zone(zonefilegen.core.get_rev_zone_name(network), default_ttl)
    included_ptr_names = set()

    # Use the same NS records for reverse zones as for the forward zones
    for (name, ttl, data) in ns_records:
        rec = zonefilegen.core.ResourceRecord()
        rec.name = '@'
        rec.record_type = 'NS'
        rec.record_class = 'IN'
        rec.ttl = ttl
        rec.data = data
        rev_zone.records.append(rec)

    for (name, ttl, addr) in ptr_candidates:
        if addr in network:
            rec = zonefilegen.core.ResourceRecord()
            rec.name = zonefilegen.core.get_rev_ptr_name(addr, network.prefixlen)
            rec.record_type = 'PTR'
            rec.record_class = 'IN'
            rec.ttl = ttl
            rec.data = name
            if rec.name not in included_ptr_names:
                included_ptr_names.add(rec.name)
                rev_zone.records.append(rec)
    return rev_zone


def build_fwd_zone(zone_name: str, rrset_dict: dict, default_ttl: int) -> zonefilegen.core.Zone:
    """
    Builds a forward DNS zone from a set of records
    """
    fwd_zone = zonefilegen.core.Zone(zone_name, default_ttl)

    for rrset in rrset_dict:
        if rrset['name'].endswith('.'):
            name = rrset['name']
        elif rrset['name'] == '@':
            name = zone_name
        else:
            name = f"{rrset['name']}.{zone_name}"

        if type(rrset['data']) is list:
            record_data_arr = rrset['data']
        else:
            record_data_arr = [rrset['data']]

        for record_data in record_data_arr:
            record = zonefilegen.core.ResourceRecord()
            record.name = name
            record.record_type = rrset['type']
            if record.record_type not in zonefilegen.core.RECORD_TYPES:
                logging.critical(f"Invalid type in record for {record.name} : {record.record_type}")
                exit(1)
            if 'ttl' in rrset:
                record.ttl = rrset['ttl']
            record.record_class = 'IN'
            record.data = record_data

            fwd_zone.records.append(record)
    return fwd_zone


def generate_header(digest: str):
    return f"; Generated by zonefilegen, INPUT_DIGEST: {digest}"


def gen_zone(zone: zonefilegen.core.Zone, output_dir: pathlib.Path, soa_dict: dict, input_digest: str):
    out_filepath: pathlib.Path = output_dir / f"{zone.name}zone"
    logging.info(f"Generating zone file {out_filepath}")
    serial_number = None
    if out_filepath.exists():
        with open(out_filepath, 'r') as f:
            first_line_matches = FIRST_LINE_PATTERN.match(f.readline())
            soa_matches = SOA_PATTERN.search(f.read(), )
            old_digest = None
            old_serial = None
            if first_line_matches:
                old_digest = first_line_matches.group('digest')
            else:
                logging.error(f"Existing zone file {out_filepath} was not generated by this tool. Aborting.")
                exit(1)

            if soa_matches:
                old_serial = soa_matches.group('serial')
            else:
                logging.warning(f"Didn't find or recognize SOA record in existing zone file {out_filepath}. Serial number will"
                                " be reset.")

            if old_serial and old_digest:
                if old_digest != input_digest:
                    serial_number = (int(old_serial) + 1) % pow(2, 32)
                    logging.info(f"Changes detected, updating serial to {serial_number}")
                else:
                    serial_number = int(old_serial)
                    logging.info(f"No changes detected, serial remains at {serial_number}")

    if serial_number is None:
        serial_number = 1
    soa_rec = zonefilegen.generation.build_soa_record(soa_dict, serial_number)

    with open(out_filepath, 'w') as f:
        f.write(zonefilegen.generation.generate_header(input_digest) + '\n')
        f.write(zone.generate_origin() + '\n')
        f.write(zone.generate_ttl() + '\n')
        f.write(soa_rec.to_line() + '\n')
        for rec in zone.records:
            f.write(rec.to_line() + '\n')
