/*
** Copyright (C) 2001-2025 Zabbix SIA
**
** This program is free software: you can redistribute it and/or modify it under the terms of
** the GNU Affero General Public License as published by the Free Software Foundation, version 3.
**
** This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
** without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
** See the GNU Affero General Public License for more details.
**
** You should have received a copy of the GNU Affero General Public License along with this program.
** If not, see <https://www.gnu.org/licenses/>.
**/

package dns

import (
	"errors"
	"fmt"
	"net"
	"strconv"
	"strings"
	"time"

	"github.com/miekg/dns"
	"golang.zabbix.com/sdk/errs"
	"golang.zabbix.com/sdk/plugin"
	"golang.zabbix.com/sdk/zbxerr"
)

const (
	base10 = 10

	tcpProtocol = "tcp"
	udpProtocol = "udp"

	inAddrSuffix   = ".in-addr.arpa"
	inAddrV6Suffix = ".ip6.arpa"
)

const (
	noneParam = iota
	firstParam
	secondParam
	thirdParam
	fourthParam
	fifthParam
	sixthParam
	seventhParam
)

var impl Plugin

var dnsTypes = map[string]uint16{
	"ANY":   dns.TypeANY,
	"A":     dns.TypeA,
	"NS":    dns.TypeNS,
	"CNAME": dns.TypeCNAME,
	"MB":    dns.TypeMB,
	"MG":    dns.TypeMG,
	"MR":    dns.TypeMR,
	"PTR":   dns.TypePTR,
	"MD":    dns.TypeMD,
	"MF":    dns.TypeMF,
	"MX":    dns.TypeMX,
	"SOA":   dns.TypeSOA,
	"NULL":  dns.TypeNULL,
	"HINFO": dns.TypeHINFO,
	"MINFO": dns.TypeMINFO,
	"TXT":   dns.TypeTXT,
	"AAAA":  dns.TypeAAAA,
	"SRV":   dns.TypeSRV,
}

type options struct {
	ip       string
	name     string
	protocol string
	dnsType  uint16
	count    int
	timeout  time.Duration
}

// Plugin -
type Plugin struct {
	plugin.Base
}

func init() {
	err := plugin.RegisterMetrics(
		&impl, "DNS",
		"net.dns", "Checks if DNS service is up.",
		"net.dns.perf", "Measures DNS query time in seconds.",
		"net.dns.record", "Performs a DNS query.",
		"net.dns.get", "Performs a DNS query. (Returns verbose response in JSON).",
	)
	if err != nil {
		panic(errs.Wrap(err, "failed to register metric"))
	}
}

// Export -
func (p *Plugin) Export(key string, params []string, ctx plugin.ContextProvider) (result interface{}, err error) {
	switch key {
	case "net.dns":
		return exportDns(params)
	case "net.dns.record":
		return exportDnsRecord(params)
	case "net.dns.perf":
		return exportDnsPerf(params)
	case "net.dns.get":
		return exportDnsGet(params)
	default:
		err = zbxerr.ErrorUnsupportedMetric

		return
	}
}

func exportDnsPerf(params []string) (result interface{}, err error) {
	time_before := time.Now()
	_, err = getDNSAnswers(params)
	if err != nil {
		if errors.Is(err, zbxerr.ErrorCannotFetchData.Unwrap()) {
			return 0, nil
		}

		return
	}

	t := time.Since(time_before).Seconds()

	return t, nil
}

func exportDns(params []string) (result interface{}, err error) {
	answer, err := getDNSAnswers(params)
	if err != nil {
		if errors.Is(err, zbxerr.ErrorCannotFetchData.Unwrap()) {
			return 0, nil
		}

		return
	}

	if len(answer) < 1 {
		return 0, nil
	}

	return 1, nil
}

func exportDnsRecord(params []string) (result interface{}, err error) {
	answer, err := getDNSAnswers(params)
	if err != nil {
		return
	}

	if len(answer) < 1 {
		return nil, zbxerr.New("Cannot perform DNS query.")
	}

	return parseAnswers(answer), nil
}

func parseAnswers(answers []dns.RR) string {
	var out string
	answersNum := len(answers)
	for i, a := range answers {
		out += fmt.Sprintf("%-20s", strings.TrimSuffix(a.Header().Name, "."))
		out += fmt.Sprintf(" %-8s ", dns.Type(a.Header().Rrtype).String())

		switch rr := a.(type) {
		case *dns.A:
			out += getAString(rr)
		case *dns.NS:
			out += getNSString(rr)
		case *dns.CNAME:
			out += getCNAMEString(rr)
		case *dns.MB:
			out += getMBString(rr)
		case *dns.MG:
			out += getMGString(rr)
		case *dns.PTR:
			out += getPTRString(rr)
		case *dns.MD:
			out += getMDString(rr)
		case *dns.MF:
			out += getMFString(rr)
		case *dns.MX:
			out += getMXString(rr)
		case *dns.SOA:
			out += getSOAString(rr)
		case *dns.NULL:
			out += getNULLString(rr)
		case *dns.HINFO:
			out += getHINFOString(rr)
		case *dns.MINFO:
			out += getMINFOString(rr)
		case *dns.TXT:
			out += getTXTString(rr)
		case *dns.AAAA:
			out += getAAAAString(rr)
		case *dns.SRV:
			out += getSRVString(rr)
		}

		if i != answersNum-1 {
			out += "\n"
		}
	}

	return out
}

func getDNSAnswers(params []string) ([]dns.RR, error) {
	options, err := parseParams(params)
	if err != nil {
		return nil, err
	}

	var resp *dns.Msg
	for i := 1; i <= options.count; i++ {
		resp, err = runQuery(options.ip, options.name, options.protocol, options.dnsType, options.timeout)
		if err != nil {
			continue
		}

		break
	}

	if err != nil {
		return nil, zbxerr.ErrorCannotFetchData.Wrap(err)
	}

	return resp.Answer, nil
}

func getSOAString(in *dns.SOA) string {
	return strings.TrimSuffix(in.Ns, ".") +
		" " + strings.TrimSuffix(in.Mbox, ".") +
		" " + strconv.FormatInt(int64(in.Serial), base10) +
		" " + strconv.FormatInt(int64(in.Refresh), base10) +
		" " + strconv.FormatInt(int64(in.Retry), base10) +
		" " + strconv.FormatInt(int64(in.Expire), base10) +
		" " + strconv.FormatInt(int64(in.Minttl), base10)
}

func getAString(in *dns.A) string {
	if in.A == nil {
		return "\n"
	}

	return in.A.String()
}

func getNSString(in *dns.NS) string {
	return strings.TrimSuffix(in.Ns, ".")
}

func getCNAMEString(in *dns.CNAME) string {
	return strings.TrimSuffix(in.Target, ".")
}

func getMBString(in *dns.MB) string {
	return strings.TrimSuffix(in.Mb, ".")
}

func getMGString(in *dns.MG) string {
	return strings.TrimSuffix(in.Mg, ".")
}

func getPTRString(in *dns.PTR) string {
	return strings.TrimSuffix(in.Ptr, ".")
}

func getMDString(in *dns.MD) string {
	return strings.TrimSuffix(in.Md, ".")
}

func getMFString(in *dns.MF) string {
	return strings.TrimSuffix(in.Mf, ".")
}

func getMXString(in *dns.MX) string {
	return strconv.Itoa(int(in.Preference)) +
		" " + strings.TrimSuffix(in.Mx, ".")
}

func getNULLString(in *dns.NULL) string {
	return strings.TrimSuffix(in.Data, ".")
}

func getHINFOString(in *dns.HINFO) string {
	return parseTXT(in.Cpu, in.Os)
}

func getMINFOString(in *dns.MINFO) string {
	return strings.TrimSuffix(in.Rmail, ".") + " " +
		strings.TrimSuffix(in.Email, ".")
}

func getTXTString(in *dns.TXT) string {
	return parseTXT(in.Txt...)
}

func getAAAAString(in *dns.AAAA) string {
	if in.AAAA == nil {
		return "\n"
	}

	return in.AAAA.String()
}

func getSRVString(in *dns.SRV) string {
	return strconv.Itoa(int(in.Priority)) + " " +
		strconv.Itoa(int(in.Weight)) + " " +
		strconv.Itoa(int(in.Port)) + " " +
		strings.TrimSuffix(in.Target, ".")
}

func parseTXT(in ...string) string {
	var out string
	for _, s := range in {
		if s != "" {
			out += "\"" + s + "\"" + " "
		}
	}

	return strings.TrimSpace(out)
}

func parseParams(params []string) (*options, error) {
	var o options

	switch len(params) {
	case sixthParam:
		err := o.setProtocol(params[sixthParam-1])
		if err != nil {
			return nil, err
		}

		fallthrough
	case fifthParam:
		err := o.setCount(params[fifthParam-1])
		if err != nil {
			return nil, err
		}

		fallthrough
	case fourthParam:
		err := o.setTimeout(params[fourthParam-1])
		if err != nil {
			return nil, err
		}

		fallthrough
	case thirdParam:
		err := o.setDNSType(params[thirdParam-1])
		if err != nil {
			return nil, err
		}

		fallthrough
	case secondParam:
		o.name = params[secondParam-1]

		fallthrough
	case firstParam:
		err := o.setIP(params[firstParam-1])
		if err != nil {
			return &o, zbxerr.New(fmt.Sprintf("invalid first parameter, %s", err.Error()))
		}

		fallthrough
	case noneParam:
		err := o.setDefaults()
		if err != nil {
			return nil, err
		}
	default:
		err := zbxerr.ErrorTooManyParameters

		return nil, err
	}

	return &o, nil
}

func (o *options) setIP(ip string) error {
	if ip == "" {
		return nil
	}

	if !isValidIP(ip) {
		return fmt.Errorf("invalid IP address, %s", ip)
	}

	o.ip = net.JoinHostPort(ip, "53")

	return nil
}

func isValidIP(ip string) bool {
	if r := net.ParseIP(ip); r == nil {
		return false
	}

	return true
}

func (o *options) setProtocol(protocol string) error {
	switch protocol {
	case tcpProtocol:
		o.protocol = tcpProtocol
	case udpProtocol, "":
		o.protocol = udpProtocol
	default:
		return zbxerr.New("invalid sixth parameter")
	}

	return nil
}

func (o *options) setCount(c string) error {
	if c == "" {
		return nil
	}

	count, err := strconv.Atoi(c)
	if err != nil {
		return zbxerr.New(fmt.Sprintf("invalid fifth parameter, %s", err.Error()))
	}

	if count <= 0 {
		return zbxerr.New("invalid fifth parameter")
	}

	o.count = count

	return nil
}

func (o *options) setTimeout(timeout string) error {
	if timeout == "" {
		return nil
	}

	t, err := strconv.Atoi(timeout)
	if err != nil {
		return zbxerr.New(fmt.Sprintf("invalid fourth parameter, %s", err.Error()))
	}

	if t <= 0 {
		return zbxerr.New("invalid fourth parameter")
	}

	o.timeout = time.Duration(t) * time.Second

	return nil
}

func (o *options) setDNSType(dnsType string) error {
	if dnsType == "" {
		return nil
	}

	t, ok := dnsTypes[strings.ToUpper(dnsType)]
	if !ok {
		return zbxerr.New(fmt.Sprintf("invalid third parameter, unknown dns type %s", dnsType))
	}

	o.dnsType = t

	return nil
}

func (o *options) setDefaults() error {
	if o.ip == "" {
		err := o.setDefaultIP()
		if err != nil {
			return zbxerr.New(err.Error())
		}
	}

	if o.name == "" {
		return zbxerr.New("second parameter cannot be empty")
	}

	if o.dnsType == dns.TypeNone {
		o.dnsType = dns.TypeSOA
	}

	if o.timeout < 1 {
		o.timeout = 1 * time.Second
	}

	if o.count < 1 {
		o.count = 2
	}

	if o.protocol == "" {
		o.protocol = udpProtocol
	}

	return nil
}

func runQuery(resolver, domain, net string, record uint16, timeout time.Duration) (*dns.Msg, error) {
	c := &dns.Client{
		Net:          net,
		DialTimeout:  timeout,
		ReadTimeout:  timeout,
		WriteTimeout: timeout,
	}

	var err error
	if record == dns.TypePTR &&
		!strings.HasSuffix(domain, inAddrSuffix) &&
		!strings.HasSuffix(domain, inAddrV6Suffix) {
		domain, err = dns.ReverseAddr(domain)
		if err != nil {
			return nil, err
		}
	}

	m := &dns.Msg{
		MsgHdr: dns.MsgHdr{
			CheckingDisabled: false,
			RecursionDesired: true,
			Opcode:           dns.OpcodeQuery,
			Rcode:            dns.RcodeSuccess,
		},
		Question: []dns.Question{
			{Name: dns.Fqdn(domain), Qtype: record, Qclass: dns.ClassINET},
		},
	}

	r, _, err := c.Exchange(m, resolver)
	if err != nil {
		return nil, err
	}

	return r, nil
}
