//go:build linux

package microvm

import (
	"context"
	"errors"
	"fmt"
	"log/slog"
	"net"
	"sync"
	"time"

	"github.com/miekg/dns"
)

const (
	dnsProxyIOTimeout       = 10 * time.Second
	dnsProxyIdleTimeout     = 30 * time.Second
	dnsProxyShutdownTimeout = 10 * time.Second
	dnsProxyMaxConnections  = 64
	dnsProxyMaxTCPQueries   = 128
	dnsProxyResolvConfPath  = "/etc/resolv.conf"
)

type DNSProxy struct {
	port uint32
	srv  *dns.Server

	closeOnce sync.Once
	closeErr  error
}

func StartDNSProxy(ctx context.Context, cid uint32, logger *slog.Logger) (*DNSProxy, error) {
	if ctx == nil {
		ctx = context.Background()
	}

	if logger == nil {
		logger = slog.Default()
	}
	logger = logger.With("where", "dns_proxy", "cid", cid)

	ln, port, err := listenRandomVsockPort(ctx)
	if err != nil {
		return nil, fmt.Errorf("listen for dns proxy: %w", err)
	}

	resolver, err := newHostDNSResolver(dnsProxyResolvConfPath, logger)
	if err != nil {
		_ = ln.Close()
		return nil, err
	}

	listener := newLimitedListener(
		&cidFilteredVsockListener{
			Listener: ln,
			cid:      cid,
			logger:   logger,
		},
		dnsProxyMaxConnections,
		logger,
	)

	proxy := &DNSProxy{
		port: port,
		srv: &dns.Server{
			Net:           "tcp",
			Listener:      listener,
			Handler:       dns.HandlerFunc(resolver.ServeDNS),
			ReadTimeout:   dnsProxyIOTimeout,
			WriteTimeout:  dnsProxyIOTimeout,
			IdleTimeout:   func() time.Duration { return dnsProxyIdleTimeout },
			MaxTCPQueries: dnsProxyMaxTCPQueries,
			MsgInvalidFunc: func(_ []byte, err error) {
				logger.Warn("dns proxy invalid message", "error", err)
			},
		},
	}

	go func() {
		<-ctx.Done()
		_ = proxy.Close()
	}()

	go func() {
		if err := proxy.srv.ActivateAndServe(); err != nil && !errors.Is(err, net.ErrClosed) {
			logger.Warn("dns proxy stopped", "error", err)
		}
	}()

	logger.Info("started dns proxy", "port", port)
	return proxy, nil
}

func (p *DNSProxy) Port() uint32 {
	if p == nil {
		return 0
	}
	return p.port
}

func (p *DNSProxy) Close() error {
	if p == nil || p.srv == nil {
		return nil
	}

	p.closeOnce.Do(func() {
		shutdownCtx, cancel := context.WithTimeout(context.Background(), dnsProxyShutdownTimeout)
		defer cancel()

		p.closeErr = p.srv.ShutdownContext(shutdownCtx)
	})
	return p.closeErr
}

type limitedListener struct {
	net.Listener
	slots  chan struct{}
	logger *slog.Logger
}

func newLimitedListener(listener net.Listener, limit int, logger *slog.Logger) net.Listener {
	if limit <= 0 {
		return listener
	}
	return &limitedListener{
		Listener: listener,
		slots:    make(chan struct{}, limit),
		logger:   logger,
	}
}

func (l *limitedListener) Accept() (net.Conn, error) {
	for {
		conn, err := l.Listener.Accept()
		if err != nil {
			return nil, err
		}

		select {
		case l.slots <- struct{}{}:
			return &limitedConn{
				Conn: conn,
				release: func() {
					<-l.slots
				},
			}, nil
		default:
			l.logger.Warn("dns proxy dropped connection because workers are busy")
			_ = conn.Close()
		}
	}
}

type limitedConn struct {
	net.Conn
	once    sync.Once
	release func()
}

func (c *limitedConn) Close() error {
	err := c.Conn.Close()
	c.once.Do(c.release)
	return err
}

type hostDNSResolver struct {
	upstreams []string
	attempts  int
	timeout   time.Duration
	logger    *slog.Logger
}

func newHostDNSResolver(path string, logger *slog.Logger) (*hostDNSResolver, error) {
	config, err := dns.ClientConfigFromFile(path)
	if err != nil {
		return nil, fmt.Errorf("read host resolv.conf: %w", err)
	}
	if len(config.Servers) == 0 {
		return nil, fmt.Errorf("host resolv.conf has no nameservers")
	}

	port := config.Port
	if port == "" {
		port = "53"
	}

	upstreams := make([]string, 0, len(config.Servers))
	for _, server := range config.Servers {
		upstreams = append(upstreams, net.JoinHostPort(server, port))
	}

	timeout := time.Duration(config.Timeout) * time.Second
	if timeout <= 0 {
		timeout = dnsProxyIOTimeout
	}

	return &hostDNSResolver{
		upstreams: upstreams,
		attempts:  max(config.Attempts, 1),
		timeout:   timeout,
		logger:    logger,
	}, nil
}

func (r *hostDNSResolver) ServeDNS(w dns.ResponseWriter, req *dns.Msg) {
	resp, err := r.exchange(req)
	if err != nil {
		r.logger.Warn(
			"dns upstream exchange failed",
			"question", dnsQuestionLogValue(req),
			"error", err,
		)
		if err := w.WriteMsg(rcodeResponse(req, dns.RcodeServerFailure)); err != nil {
			r.logger.Warn("dns proxy response write failed", "error", err)
		}
		return
	}

	filterDNSResponse(resp)

	if err := w.WriteMsg(resp); err != nil {
		r.logger.Warn("dns proxy response write failed", "error", err)
	}
}

func (r *hostDNSResolver) exchange(req *dns.Msg) (*dns.Msg, error) {
	var errs []error

	for range r.attempts {
		for _, upstream := range r.upstreams {
			resp, err := exchangeDNSAt(req, upstream, r.timeout)
			if err == nil {
				return resp, nil
			}
			errs = append(errs, fmt.Errorf("%s: %w", upstream, err))
		}
	}

	return nil, errors.Join(errs...)
}

func exchangeDNSAt(req *dns.Msg, addr string, timeout time.Duration) (*dns.Msg, error) {
	resp, _, err := (&dns.Client{Net: "udp", Timeout: timeout}).Exchange(req, addr)
	if err != nil {
		return nil, err
	}
	if resp == nil {
		return nil, fmt.Errorf("empty udp response")
	}
	if !resp.Truncated {
		return resp, nil
	}

	resp, _, err = (&dns.Client{Net: "tcp", Timeout: timeout}).Exchange(req, addr)
	if err != nil {
		return nil, err
	}
	if resp == nil {
		return nil, fmt.Errorf("empty tcp response")
	}
	return resp, nil
}

func filterDNSResponse(msg *dns.Msg) {
	if msg == nil {
		return
	}
	msg.Answer = filterDNSRRs(msg.Answer)
	msg.Ns = filterDNSRRs(msg.Ns)
	msg.Extra = filterDNSRRs(msg.Extra)
}

func filterDNSRRs(rrs []dns.RR) []dns.RR {
	filtered := rrs[:0]
	for _, rr := range rrs {
		if rr := filterDNSRR(rr); rr != nil {
			filtered = append(filtered, rr)
		}
	}
	return filtered
}

func filterDNSRR(rr dns.RR) dns.RR {
	switch rr := rr.(type) {
	case *dns.A:
		if isBlockedNamespaceIP(rr.A) {
			return nil
		}
	case *dns.AAAA:
		if isBlockedNamespaceIP(rr.AAAA) {
			return nil
		}
	case *dns.SVCB:
		filterSVCBValues(&rr.Value)
	case *dns.HTTPS:
		filterSVCBValues(&rr.Value)
	}
	return rr
}

// this removes any blocked namespaces in ipv4/v6 hints
func filterSVCBValues(values *[]dns.SVCBKeyValue) {
	filtered := (*values)[:0]
	for _, value := range *values {
		switch value := value.(type) {
		case *dns.SVCBIPv4Hint:
			value.Hint = filterDNSIPs(value.Hint)
			if len(value.Hint) == 0 {
				continue
			}
		case *dns.SVCBIPv6Hint:
			value.Hint = filterDNSIPs(value.Hint)
			if len(value.Hint) == 0 {
				continue
			}
		}
		filtered = append(filtered, value)
	}
	*values = filtered
}

func filterDNSIPs(ips []net.IP) []net.IP {
	filtered := ips[:0]
	for _, ip := range ips {
		if !isBlockedNamespaceIP(ip) {
			filtered = append(filtered, ip)
		}
	}
	return filtered
}

func isBlockedNamespaceIP(ip net.IP) bool {
	if ip == nil {
		return true
	}
	if ip4 := ip.To4(); ip4 != nil {
		return isBlockedByNamespaceNets(ip4, 32)
	}
	return isBlockedByNamespaceNets(ip, 128)
}

func isBlockedByNamespaceNets(ip net.IP, bits int) bool {
	for _, blockedNet := range blockedNamespaceNets {
		if blockedNet == nil {
			continue
		}

		_, blockedBits := blockedNet.Mask.Size()
		if blockedBits != bits {
			continue
		}
		if blockedNet.Contains(ip) {
			return true
		}
	}
	return false
}

func rcodeResponse(req *dns.Msg, rcode int) *dns.Msg {
	resp := new(dns.Msg)
	if req == nil {
		resp.Rcode = rcode
		return resp
	}
	resp.SetRcode(req, rcode)
	return resp
}

func dnsQuestionLogValue(msg *dns.Msg) string {
	if msg == nil || len(msg.Question) == 0 {
		return ""
	}

	q := msg.Question[0]
	qtype := dns.TypeToString[q.Qtype]
	if qtype == "" {
		qtype = fmt.Sprintf("TYPE%d", q.Qtype)
	}
	return fmt.Sprintf("%s/%s", q.Name, qtype)
}
