package zone

import (
	"fmt"
	"sync"

	"git.giaever.org/joachimmg/go-log.git/log"
	"git.giaever.org/joachimmg/m-dns/config"
	"git.giaever.org/joachimmg/m-dns/errors"
	"git.giaever.org/joachimmg/m-dns/host"
	"github.com/miekg/dns"
)

type Zone interface {
	Records(q dns.Question) []dns.RR
}

type Resource struct {
	sync.Mutex
	host.Host
}

func (r *Resource) qType(q dns.Question) string {
	switch q.Qtype {
	case dns.TypeANY:
		return "TypeANY"
	case dns.TypePTR:
		return "TypePTR"
	case dns.TypeSRV:
		return "TypeSRV"
	case dns.TypeA:
		return "TypeA"
	case dns.TypeAAAA:
		return "TypeAAAA"
	case dns.TypeTXT:
		return "TypeTXT"
	default:
		return fmt.Sprintf("%d", q.Qtype)
	}
}

func New(h host.Host) (*Resource, error) {

	r := new(Resource)

	if r == nil {
		log.Traceln(errors.Zone, errors.OutOfMemory)
		return nil, errors.OutOfMemory
	}

	if h == nil {
		log.Traceln(errors.Zone, errors.HostIsNil)
		return nil, errors.HostIsNil
	}

	r.Host = h

	return r, nil
}

func (r *Resource) hdr(n string, rt uint16) dns.RR_Header {
	return dns.RR_Header{
		Name:   n,
		Rrtype: rt,
		Class:  dns.ClassINET,
		Ttl:    config.DefaultTTL,
	}
}

func (r *Resource) ptrRecord(q dns.Question, p host.HostString) dns.RR {
	r.Lock()
	defer r.Unlock()
	return &dns.PTR{
		Hdr: r.hdr(q.Name, dns.TypePTR),
		Ptr: p.String(),
	}
}

func (r *Resource) srvRecord(q dns.Question) dns.RR {
	r.Lock()
	defer r.Unlock()
	log.Traceln("srvRecord")
	return &dns.SRV{
		Hdr:      r.hdr(q.Name, dns.TypeSRV),
		Priority: 10,
		Weight:   1,
		Port:     r.GetPort().Uint16(),
		Target:   r.GetHostnameAddr().String(),
	}
}

func (r *Resource) txtRecord(q dns.Question) dns.RR {
	r.Lock()
	defer r.Unlock()
	return &dns.TXT{
		Hdr: r.hdr(q.Name, dns.TypeTXT),
		Txt: r.GetTXTs(),
	}
}

func (r *Resource) records(rr ...dns.RR) []dns.RR {
	rrn := make([]dns.RR, 0)
	if rr == nil {
		return rrn
	}
	return append(rrn, rr...)
}

func (r *Resource) aRecord(q dns.Question, ip host.HostIP) dns.RR {
	r.Lock()
	defer r.Unlock()
	return &dns.A{
		Hdr: r.hdr(r.GetHostnameAddr().String(), dns.TypeA),
		A:   ip.AsIP(),
	}
}

func (r *Resource) aaaaRecord(q dns.Question, ip host.HostIP) dns.RR {
	r.Lock()
	defer r.Unlock()
	return &dns.AAAA{
		Hdr:  r.hdr(r.GetHostnameAddr().String(), dns.TypeAAAA),
		AAAA: ip.AsIP(),
	}
}

func (r *Resource) axRecords(q dns.Question, atype uint16) []dns.RR {
	a := r.records(nil)[:0]
	for _, ip := range r.GetIPs() {
		switch atype {
		case dns.TypeA:
			if _, ipType := ip.Type(); ipType == host.IPv4 {
				a = append(a, r.aRecord(q, ip))
			}
		case dns.TypeAAAA:
			if _, ipType := ip.Type(); ipType == host.IPv6 {
				a = append(a, r.aaaaRecord(q, ip))
			}
		}
	}

	if len(a) == 0 {
		return nil
	}

	return a
}

// Records: Return DNS records based on the Question
func (r *Resource) Records(q dns.Question) []dns.RR {
	log.Traceln(errors.Zone, "Records", q.Name, r.qType(q))
	switch q.Name {
	case r.GetInstanceAddr().String(): // RFC 6763, 13.3 "instance"._<service>.<domain>
		return r.iRecords(q)
	case r.GetServiceAddr().String(): // RFC 6763, 13.1 <_service>.<domain>
		return r.sRecords(q)
	case r.GetDiscoveryAddr().String():
		return r.dRecords(q)
	case r.GetHostnameAddr().String():
		switch q.Qtype {
		case dns.TypeANY, dns.TypeA, dns.TypeAAAA:
			return r.iRecords(q)
		}
		fallthrough
	default:
		return nil
	}
}

// iRecords: Instance records
func (r *Resource) iRecords(q dns.Question) []dns.RR {
	switch q.Qtype {
	case dns.TypeANY:
		return append(r.iRecords(dns.Question{
			Name:  r.GetInstanceAddr().String(),
			Qtype: dns.TypeSRV,
		}), r.iRecords(dns.Question{
			Name:  r.GetInstanceAddr().String(),
			Qtype: dns.TypeTXT,
		})...)
	case dns.TypeA:
		return r.axRecords(q, dns.TypeA)
	case dns.TypeAAAA:
		return r.axRecords(q, dns.TypeAAAA)
	case dns.TypeSRV:
		return append(
			r.records(r.srvRecord(q)),
			append(
				r.iRecords(dns.Question{
					Name:  r.GetInstanceAddr().String(),
					Qtype: dns.TypeA,
				}),
				r.iRecords(dns.Question{
					Name:  r.GetInstanceAddr().String(),
					Qtype: dns.TypeAAAA,
				})...,
			)...,
		)
	case dns.TypeTXT:
		return r.records(r.txtRecord(q))
	default:
		log.Traceln(errors.Zone, "None iRecord for", r.qType(q))
		return nil
	}
}

// sRecords: Service records
func (r *Resource) sRecords(q dns.Question) []dns.RR {
	switch q.Qtype {
	case dns.TypeANY, dns.TypePTR:
		return append(
			r.records(r.ptrRecord(q, r.GetInstanceAddr())),
			r.iRecords(dns.Question{
				Name:  r.GetInstanceAddr().String(),
				Qtype: dns.TypeANY,
			})...,
		)
	default:
		log.Traceln(errors.Zone, "None sRecord for", r.qType(q))
		return nil
	}
}

// dRecords: DNS-SD / Discovery aka enumerate
func (r *Resource) dRecords(q dns.Question) []dns.RR {
	switch q.Qtype {
	case dns.TypeANY, dns.TypePTR:
		return r.records(r.ptrRecord(q, r.GetServiceAddr()))
	default:
		log.Traceln(errors.Zone, "None dRecord for", r.qType(q))
		return nil
	}
}
