package newdns

import (
	"fmt"
	"net"

	"github.com/miekg/dns"
)

// Config provides configuration for a DNS server.
type Config struct {
	// The buffer size used if EDNS is enabled by a client.
	//
	// Default: 1220.
	BufferSize int

	// The list of zones handled by this server.
	//
	// Default: ["."].
	Zones []string

	// Handler is the callback that returns a zone for the specified name.
	// The returned zone must not be altered going forward.
	Handler func(name string) (*Zone, error)

	// The fallback DNS server to be used if the zones is not matched. Exact
	// zones must be provided above for this to work.
	Fallback string

	// Reporter is the callback called with request errors.
	Logger Logger
}

// Server is a DNS server.
type Server struct {
	config Config
	close  chan struct{}
}

// NewServer creates and returns a new DNS server.
func NewServer(config Config) *Server {
	// set default buffer size
	if config.BufferSize <= 0 {
		config.BufferSize = 1220
	}

	// set default zone
	if len(config.Zones) == 0 {
		config.Zones = []string{"."}
	}

	// check zones if fallback
	if config.Fallback != "" {
		for _, zone := range config.Zones {
			if zone == "." {
				panic(`fallback conflicts with the match all pattern "." (default)`)
			}
		}
	}

	return &Server{
		config: config,
		close:  make(chan struct{}),
	}
}

// Run will run a UDP and TCP server on the specified address. It will return
// on the first accept error and close all servers.
func (s *Server) Run(addr string) error {
	// prepare mux
	mux := dns.NewServeMux()

	// register handler
	for _, zone := range s.config.Zones {
		mux.Handle(zone, s)
	}

	// add fallback if available
	if s.config.Fallback != "" {
		mux.Handle(".", Proxy(s.config.Fallback, s.config.Logger))
	}

	// run server
	err := Run(addr, mux, Accept(s.config.Logger), s.close)
	if err != nil {
		return err
	}

	return nil
}

// ServeDNS implements the dns.Handler interface.
func (s *Server) ServeDNS(w dns.ResponseWriter, req *dns.Msg) {
	// get question
	question := req.Question[0]

	// check class
	if question.Qclass != dns.ClassINET {
		log(s.config.Logger, Ignored, nil, nil, fmt.Sprintf("unsupported class: %s", dns.ClassToString[question.Qclass]))
		return
	}

	// log request and finish
	log(s.config.Logger, Request, req, nil, "")
	defer log(s.config.Logger, Finish, nil, nil, "")

	// prepare response
	res := new(dns.Msg)
	res.SetReply(req)

	// always compress responses
	res.Compress = true

	// set flag
	res.Authoritative = true

	// check edns
	if req.IsEdns0() != nil {
		// use edns in reply
		res.SetEdns0(uint16(s.config.BufferSize), false)

		// check version
		if req.IsEdns0().Version() != 0 {
			log(s.config.Logger, Refused, nil, nil, fmt.Sprintf("unsupported EDNS version: %d", req.IsEdns0().Version()))
			s.writeError(w, req, res, nil, dns.RcodeBadVers)
			return
		}
	}

	// check any type
	if question.Qtype == dns.TypeANY {
		log(s.config.Logger, Refused, nil, nil, "unsupported type: ANY")
		s.writeError(w, req, res, nil, dns.RcodeNotImplemented)
		return
	}

	// get name
	name := NormalizeDomain(question.Name, true, false, false)

	// get zone
	zone, err := s.config.Handler(name)
	if err != nil {
		err = fmt.Errorf("server handler error: %w", err)
		log(s.config.Logger, BackendError, nil, err, "")
		s.writeError(w, req, res, nil, dns.RcodeServerFailure)
		return
	}

	// check zone
	if zone == nil {
		log(s.config.Logger, Refused, nil, nil, "no zone")
		res.Authoritative = false
		s.writeError(w, req, res, nil, dns.RcodeRefused)
		return
	}

	// validate zone
	err = zone.Validate()
	if err != nil {
		log(s.config.Logger, BackendError, nil, err, "")
		s.writeError(w, req, res, nil, dns.RcodeServerFailure)
		return
	}

	// answer SOA directly
	if question.Qtype == dns.TypeSOA && name == zone.Name {
		s.writeSOAResponse(w, req, res, zone)
		return
	}

	// answer NS directly
	if question.Qtype == dns.TypeNS && name == zone.Name {
		s.writeNSResponse(w, req, res, zone)
		return
	}

	// check type
	typ := Type(question.Qtype)

	// lookup main answer
	answer, exists, err := zone.Lookup(name, typ)
	if err != nil {
		log(s.config.Logger, BackendError, nil, err, "")
		s.writeError(w, req, res, nil, dns.RcodeServerFailure)
		return
	}

	// handle absence
	if len(answer) == 0 {
		if exists {
			// we have a record, but not of the requested type
			s.writeError(w, req, res, zone, dns.RcodeSuccess)
		} else {
			// we have no record at all for this name
			s.writeError(w, req, res, zone, dns.RcodeNameError)
		}
		return
	}

	// prepare extra set
	var extra []Set

	// TODO: Lookup glue records for NS records?

	// lookup extra sets
	for _, set := range answer {
		for _, record := range set.Records {
			switch set.Type {
			case MX, SRV:
				// lookup internal MX/SRV target A and AAAA records
				if InZone(zone.Name, record.Address) {
					ret, _, err := zone.Lookup(record.Address, A, AAAA)
					if err != nil {
						log(s.config.Logger, BackendError, nil, err, "")
						s.writeError(w, req, res, nil, dns.RcodeServerFailure)
						return
					}

					// add to extra
					extra = append(extra, ret...)
				}
			}
		}
	}

	// set answer
	for _, set := range answer {
		res.Answer = append(res.Answer, s.convert(question.Name, zone, set)...)
	}

	// set extra
	for _, set := range extra {
		res.Extra = append(res.Extra, s.convert(question.Name, zone, set)...)
	}

	// add ns records
	for _, ns := range zone.AllNameServers {
		res.Ns = append(res.Ns, &dns.NS{
			Hdr: dns.RR_Header{
				Name:   TransferCase(question.Name, zone.Name),
				Rrtype: dns.TypeNS,
				Class:  dns.ClassINET,
				Ttl:    toSeconds(zone.NSTTL),
			},
			Ns: ns,
		})
	}

	// check if NS query
	if typ == NS {
		// move answers
		res.Ns = res.Answer
		res.Answer = nil

		// no authoritative response for other zone in NS queries
		res.Authoritative = false
	}

	// write message
	s.writeMessage(w, req, res)
}

// Close will close the server.
func (s *Server) Close() {
	defer func() { recover() }()
	close(s.close)
}

func (s *Server) writeSOAResponse(w dns.ResponseWriter, rq, rs *dns.Msg, zone *Zone) {
	// add soa record
	rs.Answer = append(rs.Answer, &dns.SOA{
		Hdr: dns.RR_Header{
			Name:   zone.Name,
			Rrtype: dns.TypeSOA,
			Class:  dns.ClassINET,
			Ttl:    toSeconds(zone.SOATTL),
		},
		Ns:      zone.MasterNameServer,
		Mbox:    emailToDomain(zone.AdminEmail),
		Serial:  1,
		Refresh: toSeconds(zone.Refresh),
		Retry:   toSeconds(zone.Retry),
		Expire:  toSeconds(zone.Expire),
		Minttl:  toSeconds(zone.MinTTL),
	})

	// add ns records
	for _, ns := range zone.AllNameServers {
		rs.Ns = append(rs.Ns, &dns.NS{
			Hdr: dns.RR_Header{
				Name:   zone.Name,
				Rrtype: dns.TypeNS,
				Class:  dns.ClassINET,
				Ttl:    toSeconds(zone.NSTTL),
			},
			Ns: ns,
		})
	}

	// write message
	s.writeMessage(w, rq, rs)
}

func (s *Server) writeNSResponse(w dns.ResponseWriter, rq, rs *dns.Msg, zone *Zone) {
	// add ns records
	for _, ns := range zone.AllNameServers {
		rs.Answer = append(rs.Answer, &dns.NS{
			Hdr: dns.RR_Header{
				Name:   zone.Name,
				Rrtype: dns.TypeNS,
				Class:  dns.ClassINET,
				Ttl:    toSeconds(zone.NSTTL),
			},
			Ns: ns,
		})
	}

	// write message
	s.writeMessage(w, rq, rs)
}

func (s *Server) writeError(w dns.ResponseWriter, rq, rs *dns.Msg, zone *Zone, code int) {
	// set code
	rs.Rcode = code

	// add soa record
	if zone != nil {
		rs.Ns = append(rs.Ns, &dns.SOA{
			Hdr: dns.RR_Header{
				Name:   zone.Name,
				Rrtype: dns.TypeSOA,
				Class:  dns.ClassINET,
				Ttl:    toSeconds(zone.SOATTL),
			},
			Ns:      zone.MasterNameServer,
			Mbox:    emailToDomain(zone.AdminEmail),
			Serial:  1,
			Refresh: toSeconds(zone.Refresh),
			Retry:   toSeconds(zone.Retry),
			Expire:  toSeconds(zone.Expire),
			Minttl:  toSeconds(zone.MinTTL),
		})
	}

	// write message
	s.writeMessage(w, rq, rs)
}

func (s *Server) writeMessage(w dns.ResponseWriter, rq, rs *dns.Msg) {
	// get buffer size
	var buffer = 512
	if rq.IsEdns0() != nil {
		buffer = int(rq.IsEdns0().UDPSize())
	}

	// determine if client is using UDP
	isUDP := w.RemoteAddr().Network() == "udp"

	// truncate message if client is using UDP and message is too long
	if isUDP && rs.Len() > buffer {
		rs.Truncated = true
		rs.Answer = nil
		rs.Ns = nil
		rs.Extra = nil
	}

	// write message
	err := w.WriteMsg(rs)
	if err != nil {
		log(s.config.Logger, NetworkError, nil, err, "")
		_ = w.Close()
		return
	}

	// log response
	log(s.config.Logger, Response, rs, nil, "")
}

func (s *Server) convert(query string, zone *Zone, set Set) []dns.RR {
	// prepare header
	header := dns.RR_Header{
		Name:   TransferCase(query, set.Name),
		Rrtype: uint16(set.Type),
		Class:  dns.ClassINET,
		Ttl:    toSeconds(set.TTL),
	}

	// ensure zone min TTL
	if set.TTL < zone.MinTTL {
		header.Ttl = toSeconds(zone.MinTTL)
	}

	// prepare list
	var list []dns.RR

	// add records
	for _, record := range set.Records {
		// construct record
		switch set.Type {
		case A:
			list = append(list, &dns.A{
				Hdr: header,
				A:   net.ParseIP(record.Address),
			})
		case AAAA:
			list = append(list, &dns.AAAA{
				Hdr:  header,
				AAAA: net.ParseIP(record.Address),
			})
		case CNAME:
			list = append(list, &dns.CNAME{
				Hdr:    header,
				Target: dns.Fqdn(record.Address),
			})
		case MX:
			list = append(list, &dns.MX{
				Hdr:        header,
				Preference: uint16(record.Priority),
				Mx:         dns.Fqdn(record.Address),
			})
		case TXT:
			list = append(list, &dns.TXT{
				Hdr: header,
				Txt: record.Data,
			})
		case NS:
			list = append(list, &dns.NS{
				Hdr: header,
				Ns:  dns.Fqdn(record.Address),
			})
		case SRV:
			list = append(list, &dns.SRV{
				Hdr:      header,
				Priority: uint16(record.Priority),
				Weight:   uint16(record.Weight),
				Port:     uint16(record.Port),
				Target:   dns.Fqdn(record.Address),
			})
		}
	}

	return list
}
