package probers

import (
	"fmt"
	"net"
	"net/netip"
	"slices"
	"strconv"
	"strings"

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

	"github.com/letsencrypt/boulder/observer/probers"
	"github.com/letsencrypt/boulder/strictyaml"
)

var (
	validQTypes = map[string]uint16{"A": 1, "TXT": 16, "AAAA": 28, "CAA": 257}
)

// DNSConf is exported to receive YAML configuration
type DNSConf struct {
	Proto   string `yaml:"protocol"`
	Server  string `yaml:"server"`
	Recurse bool   `yaml:"recurse"`
	QName   string `yaml:"query_name"`
	QType   string `yaml:"query_type"`
}

// Kind returns a name that uniquely identifies the `Kind` of `Configurer`.
func (c DNSConf) Kind() string {
	return "DNS"
}

// UnmarshalSettings constructs a DNSConf object from YAML as bytes.
func (c DNSConf) UnmarshalSettings(settings []byte) (probers.Configurer, error) {
	var conf DNSConf
	err := strictyaml.Unmarshal(settings, &conf)
	if err != nil {
		return nil, err
	}
	return conf, nil
}

func (c DNSConf) validateServer() error {
	server := strings.Trim(strings.ToLower(c.Server), " ")
	// Ensure `server` contains a port.
	host, port, err := net.SplitHostPort(server)
	if err != nil || port == "" {
		return fmt.Errorf(
			"invalid `server`, %q, could not be split: %s", c.Server, err)
	}
	// Ensure `server` port is valid.
	portNum, err := strconv.Atoi(port)
	if err != nil {
		return fmt.Errorf(
			"invalid `server`, %q, port must be a number", c.Server)
	}
	if portNum <= 0 || portNum > 65535 {
		return fmt.Errorf(
			"invalid `server`, %q, port number must be one in [1-65535]", c.Server)
	}
	// Ensure `server` is a valid FQDN or IP address.
	_, err = netip.ParseAddr(host)
	FQDN := dns.IsFqdn(dns.Fqdn(host))
	if err != nil && !FQDN {
		return fmt.Errorf(
			"invalid `server`, %q, is not an FQDN or IP address", c.Server)
	}
	return nil
}

func (c DNSConf) validateProto() error {
	validProtos := []string{"udp", "tcp"}
	proto := strings.Trim(strings.ToLower(c.Proto), " ")
	if slices.Contains(validProtos, proto) {
		return nil
	}
	return fmt.Errorf(
		"invalid `protocol`, got: %q, expected one in: %s", c.Proto, validProtos)
}

func (c DNSConf) validateQType() error {
	validQTypes = map[string]uint16{"A": 1, "TXT": 16, "AAAA": 28, "CAA": 257}
	qtype := strings.Trim(strings.ToUpper(c.QType), " ")
	q := make([]string, 0, len(validQTypes))
	for i := range validQTypes {
		q = append(q, i)
		if qtype == i {
			return nil
		}
	}
	return fmt.Errorf(
		"invalid `query_type`, got: %q, expected one in %s", c.QType, q)
}

// MakeProber constructs a `DNSProbe` object from the contents of the
// bound `DNSConf` object. If the `DNSConf` cannot be validated, an
// error appropriate for end-user consumption is returned instead.
func (c DNSConf) MakeProber(_ map[string]prometheus.Collector) (probers.Prober, error) {
	// validate `query_name`
	if !dns.IsFqdn(dns.Fqdn(c.QName)) {
		return nil, fmt.Errorf(
			"invalid `query_name`, %q is not an fqdn", c.QName)
	}

	// validate `server`
	err := c.validateServer()
	if err != nil {
		return nil, err
	}

	// validate `protocol`
	err = c.validateProto()
	if err != nil {
		return nil, err
	}

	// validate `query_type`
	err = c.validateQType()
	if err != nil {
		return nil, err
	}

	return DNSProbe{
		proto:   strings.Trim(strings.ToLower(c.Proto), " "),
		recurse: c.Recurse,
		qname:   c.QName,
		server:  c.Server,
		qtype:   validQTypes[strings.Trim(strings.ToUpper(c.QType), " ")],
	}, nil
}

// Instrument is a no-op to implement the `Configurer` interface.
func (c DNSConf) Instrument() map[string]prometheus.Collector {
	return nil
}

// init is called at runtime and registers `DNSConf`, a `Prober`
// `Configurer` type, as "DNS".
func init() {
	probers.Register(DNSConf{})
}
