package server

import (
	"context"
	"errors"
	"log"
	"sync"
	"time"

	"github.com/abh/geodns/v3/appconfig"
	"github.com/abh/geodns/v3/monitor"
	"github.com/abh/geodns/v3/querylog"
	"github.com/abh/geodns/v3/zones"
	"go.ntppool.org/common/version"
	"golang.org/x/sync/errgroup"

	"github.com/miekg/dns"
	"github.com/prometheus/client_golang/prometheus"
)

type serverMetrics struct {
	Queries *prometheus.CounterVec
}

// Server ...
type Server struct {
	PublicDebugQueries bool
	DetailedMetrics    bool

	queryLogger querylog.QueryLogger
	mux         *dns.ServeMux
	info        *monitor.ServerInfo
	metrics     *serverMetrics

	lock       sync.Mutex
	dnsServers []*dns.Server
}

// NewServer ...
func NewServer(config *appconfig.AppConfig, si *monitor.ServerInfo) *Server {
	mux := dns.NewServeMux()

	queries := prometheus.NewCounterVec(
		prometheus.CounterOpts{
			Name: "dns_queries_total",
			Help: "Number of served queries",
		},
		[]string{"zone", "qtype", "qname", "rcode"},
	)
	prometheus.MustRegister(queries)

	version.RegisterMetric("geodns", prometheus.DefaultRegisterer)

	instanceInfo := prometheus.NewGaugeVec(
		prometheus.GaugeOpts{
			Name: "geodns_instance_info",
			Help: "GeoDNS instance information",
		},
		[]string{"ID", "IP", "Group"},
	)
	prometheus.MustRegister(instanceInfo)
	group := ""
	if len(si.Groups) > 0 {
		group = si.Groups[0]
	}
	instanceInfo.WithLabelValues(si.ID, si.IP, group).Set(1)

	startTime := prometheus.NewGauge(
		prometheus.GaugeOpts{
			Name: "geodns_start_time_seconds",
			Help: "Unix time process started",
		},
	)
	prometheus.MustRegister(startTime)

	nano := si.Started.UnixNano()
	startTime.Set(float64(nano) / 1e9)

	metrics := &serverMetrics{
		Queries: queries,
	}

	return &Server{
		PublicDebugQueries: appconfig.Config.DNS.PublicDebugQueries,
		DetailedMetrics:    appconfig.Config.DNS.DetailedMetrics,

		mux:     mux,
		info:    si,
		metrics: metrics,
	}
}

// SetQueryLogger configures the query logger. For now it only supports writing to
// a file (and all zones get logged to the same file).
func (srv *Server) SetQueryLogger(logger querylog.QueryLogger) {
	srv.queryLogger = logger
}

// Add adds the Zone to be handled under the specified name
func (srv *Server) Add(name string, zone *zones.Zone) {
	srv.mux.HandleFunc(name, srv.setupServerFunc(zone))
}

// Remove removes the zone name from being handled by the server
func (srv *Server) Remove(name string) {
	srv.mux.HandleRemove(name)
}

func (srv *Server) setupServerFunc(zone *zones.Zone) func(dns.ResponseWriter, *dns.Msg) {
	return func(w dns.ResponseWriter, r *dns.Msg) {
		srv.serve(w, r, zone)
	}
}

// ServeDNS calls ServeDNS in the dns package
func (srv *Server) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
	srv.mux.ServeDNS(w, r)
}

func (srv *Server) addDNSServer(dnsServer *dns.Server) {
	srv.lock.Lock()
	defer srv.lock.Unlock()
	srv.dnsServers = append(srv.dnsServers, dnsServer)
}

// ListenAndServe starts the DNS server on the specified IP
// (both tcp and udp). It returns an error if
// something goes wrong.
func (srv *Server) ListenAndServe(ctx context.Context, ip string) error {

	prots := []string{"udp", "tcp"}

	g, _ := errgroup.WithContext(ctx)

	for _, prot := range prots {

		p := prot

		g.Go(func() error {
			server := &dns.Server{
				Addr:    ip,
				Net:     p,
				Handler: srv,
			}

			srv.addDNSServer(server)

			log.Printf("Opening on %s %s", ip, p)
			if err := server.ListenAndServe(); err != nil {
				log.Printf("geodns: failed to setup %s %s: %s", ip, p, err)
				return err
			}
			return nil
		})
	}

	// the servers will be shutdown when Shutdown() is called
	return g.Wait()
}

// Shutdown gracefully shuts down the server
func (srv *Server) Shutdown() error {
	var errs []error

	for _, dnsServer := range srv.dnsServers {
		timeoutCtx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
		defer cancel()
		err := dnsServer.ShutdownContext(timeoutCtx)
		if err != nil {
			errs = append(errs, err)
		}
	}

	if srv.queryLogger != nil {
		err := srv.queryLogger.Close()
		if err != nil {
			errs = append(errs, err)
		}
	}

	err := errors.Join(errs...)

	return err
}
