package server

import (
	"net"
	"os"
	"os/signal"
	"strings"
	"sync"
	"time"

	"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/zone"
	"github.com/miekg/dns"
)

type MDnsServer struct {
	sync.Mutex
	zone.Zone
	*net.Interface

	ipv4 *net.UDPConn
	ipv6 *net.UDPConn

	running bool
	runCh   chan struct{}
}

func New(z zone.Zone, iface *net.Interface) (*MDnsServer, error) {
	m := new(MDnsServer)

	if m == nil {
		log.Traceln(errors.Server, errors.OutOfMemory)
		return nil, errors.OutOfMemory
	}

	if z == nil {
		log.Traceln(errors.Server, errors.ServerIsNil)
		return nil, errors.ServerIsNil
	}

	m.Zone = z
	m.Interface = iface

	var err error

	if m.ipv4, err = net.ListenMulticastUDP("udp4", m.Interface, config.MdnsIPv4Addr); err != nil {
		log.Infoln(errors.Server, config.MdnsIPv4, err)
	}

	if m.ipv6, err = net.ListenMulticastUDP("udp6", m.Interface, config.MdnsIPv6Addr); err != nil {
		log.Infoln(errors.Server, config.MdnsIPv6, err)
	}

	if m.ipv4 == nil && m.ipv6 == nil {
		log.Traceln(errors.Server, errors.ServerNoListenersStarted)
		return nil, errors.ServerNoListenersStarted
	}

	m.runCh = make(chan struct{})
	m.running = true

	go m.recv(m.ipv4)
	go m.recv(m.ipv6)

	return m, nil
}

func (m *MDnsServer) shutdownListener() {
	log.Traceln("Shutdown listener set on ctrl+x")
	c := make(chan os.Signal, 1)
	signal.Notify(c, os.Interrupt)
	go func() {
		for range c {
			m.Shutdown()
		}
	}()
}

func (m *MDnsServer) Shutdown() {
	log.Traceln("Shutting down MDNS-server")
	m.Lock()
	defer m.Unlock()

	if !m.running {
		return
	}

	if m.ipv4 != nil {
		m.ipv4.Close()
	}

	if m.ipv6 != nil {
		m.ipv6.Close()
	}

	m.running = false
	close(m.runCh)
}

func (m *MDnsServer) Daemon() {
	log.Traceln("Daemon running.")
	go m.shutdownListener()
	<-m.runCh
	log.Traceln("Daemon ending.")
}

func (m *MDnsServer) recv(i *net.UDPConn) {
	if i == nil {
		return
	}

	buf := make([]byte, config.BufSize)

	for m.running {
		m.Lock()
		i.SetReadDeadline(time.Now().Add(config.BufReadDeadline * time.Second))
		_, addr, err := i.ReadFromUDP(buf)
		m.Unlock()

		if err != nil {
			if !strings.Contains(err.Error(), "i/o timeout") {
				log.Traceln(errors.Server, err)
			}
			continue
		}

		go m.handlePacket(buf, addr)

	}
}

func (m *MDnsServer) send(msg *dns.Msg, addr *net.UDPAddr) (int, error) {

	if msg == nil {
		return 0, nil
	}

	buf, err := msg.Pack()

	if err != nil {
		return 0, err
	}

	if addr.IP.To4() != nil {
		return m.ipv4.WriteToUDP(buf, addr)
	}

	return m.ipv6.WriteToUDP(buf, addr)
}

func (m *MDnsServer) handlePacket(p []byte, addr *net.UDPAddr) {
	msg := new(dns.Msg)

	if err := msg.Unpack(p); err != nil {
		log.Warningln(errors.Server, addr, err)
	}

	umsg, mmsg, err := m.handleMsg(msg)

	if err != nil {
		log.Warningln(errors.Server, addr, err)
		return
	}

	if n, err := m.send(umsg, addr); err != nil {
		log.Warningln(errors.Server, "Wrote", n, err)
	}

	if n, err := m.send(mmsg, addr); err != nil {
		log.Warningln(errors.Server, "Wrote", n, err)
	}
}

func (m *MDnsServer) handleMsg(msg *dns.Msg) (*dns.Msg, *dns.Msg, error) {
	if msg.Opcode != dns.OpcodeQuery {
		log.Traceln(errors.Server, errors.ServerReceivedNonQueryOpcode)
		return nil, nil, errors.ServerReceivedNonQueryOpcode
	}

	if msg.Rcode != 0 {
		log.Traceln(errors.Server, errors.ServerReceivedNonZeroRcode)
		return nil, nil, errors.ServerReceivedNonZeroRcode
	}

	if msg.Truncated {
		log.Traceln(errors.Server, errors.ServerReceivedTruncatedSet)
		return nil, nil, errors.ServerReceivedTruncatedSet
	}

	var uAnswer, mAnswer []dns.RR

	for _, q := range msg.Question {
		uRecords, mRecords := m.handleQuestion(q)
		uAnswer = append(uAnswer, uRecords...)
		mAnswer = append(mAnswer, mRecords...)
	}

	return m.handleResponse(msg, true, uAnswer), m.handleResponse(msg, false, mAnswer), nil
}

func (m *MDnsServer) handleQuestion(question dns.Question) ([]dns.RR, []dns.RR) {
	r := m.Records(question)

	if len(r) == 0 {
		return nil, nil
	}

	for i, rec := range r {
		log.Traceln(errors.Server, "Record", i, rec)
	}

	if question.Qclass&(1<<15) != 0 || config.ForceUnicast {
		return r, nil
	}

	return nil, r
}

func (m *MDnsServer) handleResponse(msg *dns.Msg, uni bool, ans []dns.RR) *dns.Msg {
	id := uint16(0)
	if uni {
		id = msg.Id
	}

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

	return &dns.Msg{
		MsgHdr: dns.MsgHdr{
			Id:            id,
			Response:      true,
			Opcode:        dns.OpcodeQuery,
			Authoritative: true,
		},
		Compress: true,
		Answer:   ans,
	}
}
