package newdns

import (
	"errors"
	"fmt"
	"time"
)

// Zone describes a single authoritative DNS zone.
type Zone struct {
	// The FQDN of the zone e.g. "example.com.".
	Name string

	// The FQDN of the master mame server responsible for this zone. The FQDN
	// must be returned as A and AAAA record by the parent zone.
	MasterNameServer string

	// A list of FQDNs to all authoritative name servers for this zone. The
	// FQDNs must be returned as A and AAAA records by the parent zone. It is
	// required to announce at least two distinct name servers per zone.
	AllNameServers []string

	// The email address of the administrator e.g. "hostmaster@example.com".
	//
	// Default: "hostmaster@NAME".
	AdminEmail string

	// The refresh interval.
	//
	// Default: 6h.
	Refresh time.Duration

	// The retry interval for the zone.
	//
	// Default: 1h.
	Retry time.Duration

	// The expiration interval of the zone.
	//
	// Default: 72h.
	Expire time.Duration

	// The TTL for the SOA record.
	//
	// Default: 15m.
	SOATTL time.Duration

	// The TTL for NS records.
	//
	// Default: 48h.
	NSTTL time.Duration

	// The minimum TTL for all records. Either this value, or the SOATTL if lower,
	// is used to determine the "negative caching TTL" which is the duration
	// caches are allowed to cache missing records (NXDOMAIN).
	//
	// Default: 5min.
	MinTTL time.Duration

	// The handler that responds to requests for this zone. The returned sets
	// must not be altered going forward.
	Handler func(name string) ([]Set, error)
}

// Validate will validate the zone and ensure the documented defaults.
func (z *Zone) Validate() error {
	// check name
	if !IsDomain(z.Name, true) {
		return fmt.Errorf("name not fully qualified: %s", z.Name)
	}

	// check master name server
	if !IsDomain(z.MasterNameServer, true) {
		return fmt.Errorf("master server not full qualified: %s", z.MasterNameServer)
	}

	// check name server count
	if len(z.AllNameServers) < 1 {
		return fmt.Errorf("missing name servers")
	}

	// check name servers
	var includesMaster bool
	for _, ns := range z.AllNameServers {
		if !IsDomain(ns, true) {
			return fmt.Errorf("name server not fully qualified: %s", ns)
		}

		if ns == z.MasterNameServer {
			includesMaster = true
		}
	}

	// check master inclusion
	if !includesMaster {
		return fmt.Errorf("master name server not listed as name server: %s", z.MasterNameServer)
	}

	// set default admin email
	if z.AdminEmail == "" {
		z.AdminEmail = fmt.Sprintf("hostmaster@%s", z.Name)
	}

	// check admin email
	if !IsDomain(emailToDomain(z.AdminEmail), true) {
		return fmt.Errorf("admin email cannot be converted to a domain name: %s", z.AdminEmail)
	}

	// set default refresh
	if z.Refresh == 0 {
		z.Refresh = 6 * time.Hour
	}

	// set default retry
	if z.Retry == 0 {
		z.Retry = time.Hour
	}

	// set default expire
	if z.Expire == 0 {
		z.Expire = 72 * time.Hour
	}

	// set default SOA TTL
	if z.SOATTL == 0 {
		z.SOATTL = 15 * time.Minute
	}

	// set default NS TTL
	if z.NSTTL == 0 {
		z.NSTTL = 48 * time.Hour
	}

	// set default min TTL
	if z.MinTTL == 0 {
		z.MinTTL = 5 * time.Minute
	}

	// check retry
	if z.Retry >= z.Refresh {
		return fmt.Errorf("retry must be less than refresh: %d", z.Retry)
	}

	// check expire
	if z.Expire < z.Refresh+z.Retry {
		return fmt.Errorf("expire must be bigger than the sum of refresh and retry: %d", z.Expire)
	}

	return nil
}

// Lookup will lookup the specified name in the zone and return results for the
// specified record types. If no results are returned, the second return value
// indicates if there are other results for the specified name.
func (z *Zone) Lookup(name string, needle ...Type) ([]Set, bool, error) {
	// check name
	if !IsDomain(name, true) {
		return nil, false, fmt.Errorf("invalid name: %s", name)
	}

	// normalize name
	name = NormalizeDomain(name, true, false, false)

	// check name
	if !InZone(z.Name, name) {
		return nil, false, fmt.Errorf("name does not belong to zone: %s", name)
	}

	// prepare result
	var result []Set

	for i := 0; ; i++ {
		// get sets
		sets, err := z.Handler(TrimZone(z.Name, name))
		if err != nil {
			return nil, false, fmt.Errorf("zone handler error: %w", err)
		}

		// return immediately if initial set is empty
		if i == 0 && len(sets) == 0 {
			return nil, false, nil
		}

		// prepare counters
		counters := map[Type]int{
			A:     0,
			AAAA:  0,
			CNAME: 0,
			MX:    0,
			TXT:   0,
			SRV:   0,
		}

		// validate sets
		for _, set := range sets {
			// validate set
			err = set.Validate()
			if err != nil {
				return nil, false, fmt.Errorf("invalid set: %w", err)
			}

			// check relationship
			if !InZone(z.Name, set.Name) {
				return nil, false, fmt.Errorf("set does not belong to zone: %s", set.Name)
			}

			// increment counter
			counters[set.Type]++
		}

		// check counters
		for _, counter := range counters {
			if counter > 1 {
				return nil, false, errors.New("multiple sets for same type")
			}
		}

		// check apex CNAME
		if counters[CNAME] > 0 && name == z.Name {
			return nil, false, fmt.Errorf("invalid CNAME set at apex: %s", name)
		}

		// check CNAME is stand-alone
		if counters[CNAME] > 0 && (len(sets) > 1) {
			return nil, false, fmt.Errorf("other sets with CNAME set: %s", name)
		}

		// check if CNAME and query is not CNAME
		if counters[CNAME] > 0 && !typeInList(needle, CNAME) {
			// add set to result
			result = append(result, sets[0])

			// get normalized address
			address := NormalizeDomain(sets[0].Records[0].Address, true, false, false)

			// continue lookup with CNAME address if address is in zone
			if InZone(z.Name, address) {
				name = address
				continue
			}

			return result, false, nil
		}

		// add matching set
		for _, set := range sets {
			if typeInList(needle, set.Type) {
				// add set to result
				result = append(result, set)

				break
			}
		}

		// return if there are no matches, but indicate that there are sets
		// available for other types
		if len(result) == 0 {
			return nil, true, nil
		}

		return result, false, nil
	}
}
