// SPDX-License-Identifier: GPL-3.0-or-later

package unbound

import (
	"fmt"
	"strings"

	"github.com/netdata/netdata/go/go.d.plugin/agent/module"

	"golang.org/x/text/cases"
	"golang.org/x/text/language"
)

type (
	// Charts is an alias for module.Charts
	Charts = module.Charts
	// Chart is an alias for module.Charts
	Chart = module.Chart
	// Dims is an alias for module.Dims
	Dims = module.Dims
	// Dim is an alias for module.Dim
	Dim = module.Dim
)

const (
	prioQueries = module.Priority + iota
	prioIPRateLimitedQueries
	prioQueryType
	prioQueryClass
	prioQueryOpCode
	prioQueryFlag
	prioDNSCryptQueries

	prioRecurReplies
	prioReplyRCode

	prioRecurTime

	prioCache
	prioCachePercentage
	prioCachePrefetch
	prioCacheExpired
	prioZeroTTL
	prioCacheCount

	prioReqListUsage
	prioReqListCurUsage
	prioReqListJostle

	prioTCPUsage

	prioMemCache
	prioMemMod
	prioMemStreamWait
	prioUptime

	prioThread
)

func charts(cumulative bool) *Charts {
	return &Charts{
		makeIncrIf(queriesChart.Copy(), cumulative),
		makeIncrIf(ipRateLimitedQueriesChart.Copy(), cumulative),
		makeIncrIf(cacheChart.Copy(), cumulative),
		makePercOfIncrIf(cachePercentageChart.Copy(), cumulative),
		makeIncrIf(prefetchChart.Copy(), cumulative),
		makeIncrIf(expiredChart.Copy(), cumulative),
		makeIncrIf(zeroTTLChart.Copy(), cumulative),
		makeIncrIf(dnsCryptChart.Copy(), cumulative),
		makeIncrIf(recurRepliesChart.Copy(), cumulative),
		recurTimeChart.Copy(),
		reqListUsageChart.Copy(),
		reqListCurUsageChart.Copy(),
		makeIncrIf(reqListJostleChart.Copy(), cumulative),
		tcpUsageChart.Copy(),
		uptimeChart.Copy(),
	}
}

func extendedCharts(cumulative bool) *Charts {
	return &Charts{
		memCacheChart.Copy(),
		memModChart.Copy(),
		memStreamWaitChart.Copy(),
		cacheCountChart.Copy(),
		makeIncrIf(queryTypeChart.Copy(), cumulative),
		makeIncrIf(queryClassChart.Copy(), cumulative),
		makeIncrIf(queryOpCodeChart.Copy(), cumulative),
		makeIncrIf(queryFlagChart.Copy(), cumulative),
		makeIncrIf(answerRCodeChart.Copy(), cumulative),
	}
}

func threadCharts(thread string, cumulative bool) *Charts {
	charts := charts(cumulative)
	_ = charts.Remove(uptimeChart.ID)

	for i, chart := range *charts {
		convertTotalChartToThread(chart, thread, prioThread+i)
	}
	return charts
}

func convertTotalChartToThread(chart *Chart, thread string, priority int) {
	chart.ID = fmt.Sprintf("%s_%s", thread, chart.ID)
	chart.Title = fmt.Sprintf("%s %s",
		cases.Title(language.English, cases.Compact).String(thread),
		chart.Title,
	)
	chart.Fam = thread + "_stats"
	chart.Ctx = "thread_" + chart.Ctx
	chart.Priority = priority
	for _, dim := range chart.Dims {
		dim.ID = strings.Replace(dim.ID, "total", thread, 1)
	}
}

// Common stats charts
// see https://nlnetlabs.nl/documentation/unbound/unbound-control for the stats provided by unbound-control
var (
	queriesChart = Chart{
		ID:       "queries",
		Title:    "Received Queries",
		Units:    "queries",
		Fam:      "queries",
		Ctx:      "unbound.queries",
		Priority: prioQueries,
		Dims: Dims{
			{ID: "total.num.queries", Name: "queries"},
		},
	}
	ipRateLimitedQueriesChart = Chart{
		ID:       "queries_ip_ratelimited",
		Title:    "Rate Limited Queries",
		Units:    "queries",
		Fam:      "queries",
		Ctx:      "unbound.queries_ip_ratelimited",
		Priority: prioIPRateLimitedQueries,
		Dims: Dims{
			{ID: "total.num.queries_ip_ratelimited", Name: "ratelimited"},
		},
	}
	// ifdef USE_DNSCRYPT
	dnsCryptChart = Chart{
		ID:       "dnscrypt_queries",
		Title:    "DNSCrypt Queries",
		Units:    "queries",
		Fam:      "queries",
		Ctx:      "unbound.dnscrypt_queries",
		Priority: prioDNSCryptQueries,
		Dims: Dims{
			{ID: "total.num.dnscrypt.crypted", Name: "crypted"},
			{ID: "total.num.dnscrypt.cert", Name: "cert"},
			{ID: "total.num.dnscrypt.cleartext", Name: "cleartext"},
			{ID: "total.num.dnscrypt.malformed", Name: "malformed"},
		},
	}
	cacheChart = Chart{
		ID:       "cache",
		Title:    "Cache Statistics",
		Units:    "events",
		Fam:      "cache",
		Ctx:      "unbound.cache",
		Type:     module.Stacked,
		Priority: prioCache,
		Dims: Dims{
			{ID: "total.num.cachehits", Name: "hits"},
			{ID: "total.num.cachemiss", Name: "miss"},
		},
	}
	cachePercentageChart = Chart{
		ID:       "cache_percentage",
		Title:    "Cache Statistics Percentage",
		Units:    "percentage",
		Fam:      "cache",
		Ctx:      "unbound.cache_percentage",
		Type:     module.Stacked,
		Priority: prioCachePercentage,
		Dims: Dims{
			{ID: "total.num.cachehits", Name: "hits", Algo: module.PercentOfAbsolute},
			{ID: "total.num.cachemiss", Name: "miss", Algo: module.PercentOfAbsolute},
		},
	}
	prefetchChart = Chart{
		ID:       "cache_prefetch",
		Title:    "Cache Prefetches",
		Units:    "prefetches",
		Fam:      "cache",
		Ctx:      "unbound.prefetch",
		Priority: prioCachePrefetch,
		Dims: Dims{
			{ID: "total.num.prefetch", Name: "prefetches"},
		},
	}
	expiredChart = Chart{
		ID:       "cache_expired",
		Title:    "Replies Served From Expired Cache",
		Units:    "replies",
		Fam:      "cache",
		Ctx:      "unbound.expired",
		Priority: prioCacheExpired,
		Dims: Dims{
			{ID: "total.num.expired", Name: "expired"},
		},
	}
	zeroTTLChart = Chart{
		ID:       "zero_ttl_replies",
		Title:    "Replies Served From Expired Cache",
		Units:    "replies",
		Fam:      "cache",
		Ctx:      "unbound.zero_ttl_replies",
		Priority: prioZeroTTL,
		Dims: Dims{
			{ID: "total.num.zero_ttl", Name: "zero_ttl"},
		},
	}
	recurRepliesChart = Chart{
		ID:       "recursive_replies",
		Title:    "Replies That Needed Recursive Processing",
		Units:    "replies",
		Fam:      "replies",
		Ctx:      "unbound.recursive_replies",
		Priority: prioRecurReplies,
		Dims: Dims{
			{ID: "total.num.recursivereplies", Name: "recursive"},
		},
	}
	recurTimeChart = Chart{
		ID:       "recursion_time",
		Title:    "Time Spent On Recursive Processing",
		Units:    "milliseconds",
		Fam:      "recursion timings",
		Ctx:      "unbound.recursion_time",
		Priority: prioRecurTime,
		Dims: Dims{
			{ID: "total.recursion.time.avg", Name: "avg"},
			{ID: "total.recursion.time.median", Name: "median"},
		},
	}
	reqListUsageChart = Chart{
		ID:       "request_list_usage",
		Title:    "Request List Usage",
		Units:    "queries",
		Fam:      "request list",
		Ctx:      "unbound.request_list_usage",
		Priority: prioReqListUsage,
		Dims: Dims{
			{ID: "total.requestlist.avg", Name: "avg", Div: 1000},
			{ID: "total.requestlist.max", Name: "max"}, // all time max in cumulative mode, never resets
		},
	}
	reqListCurUsageChart = Chart{
		ID:       "current_request_list_usage",
		Title:    "Current Request List Usage",
		Units:    "queries",
		Fam:      "request list",
		Ctx:      "unbound.current_request_list_usage",
		Type:     module.Area,
		Priority: prioReqListCurUsage,
		Dims: Dims{
			{ID: "total.requestlist.current.all", Name: "all"},
			{ID: "total.requestlist.current.user", Name: "user"},
		},
	}
	reqListJostleChart = Chart{
		ID:       "request_list_jostle_list",
		Title:    "Request List Jostle List Events",
		Units:    "queries",
		Fam:      "request list",
		Ctx:      "unbound.request_list_jostle_list",
		Priority: prioReqListJostle,
		Dims: Dims{
			{ID: "total.requestlist.overwritten", Name: "overwritten"},
			{ID: "total.requestlist.exceeded", Name: "dropped"},
		},
	}
	tcpUsageChart = Chart{
		ID:       "tcpusage",
		Title:    "TCP Handler Buffers",
		Units:    "buffers",
		Fam:      "tcp buffers",
		Ctx:      "unbound.tcpusage",
		Priority: prioTCPUsage,
		Dims: Dims{
			{ID: "total.tcpusage", Name: "usage"},
		},
	}
	uptimeChart = Chart{
		ID:       "uptime",
		Title:    "Uptime",
		Units:    "seconds",
		Fam:      "uptime",
		Ctx:      "unbound.uptime",
		Priority: prioUptime,
		Dims: Dims{
			{ID: "time.up", Name: "time"},
		},
	}
)

// Extended stats charts
var (
	// TODO: do not add dnscrypt stuff by default?
	memCacheChart = Chart{
		ID:       "cache_memory",
		Title:    "Cache Memory",
		Units:    "KB",
		Fam:      "mem",
		Ctx:      "unbound.cache_memory",
		Type:     module.Stacked,
		Priority: prioMemCache,
		Dims: Dims{
			{ID: "mem.cache.message", Name: "message", Div: 1024},
			{ID: "mem.cache.rrset", Name: "rrset", Div: 1024},
			{ID: "mem.cache.dnscrypt_nonce", Name: "dnscrypt_nonce", Div: 1024},                 // ifdef USE_DNSCRYPT
			{ID: "mem.cache.dnscrypt_shared_secret", Name: "dnscrypt_shared_secret", Div: 1024}, // ifdef USE_DNSCRYPT
		},
	}
	// TODO: do not add subnet and ipsecmod stuff by default?
	memModChart = Chart{
		ID:       "mod_memory",
		Title:    "Module Memory",
		Units:    "KB",
		Fam:      "mem",
		Ctx:      "unbound.mod_memory",
		Type:     module.Stacked,
		Priority: prioMemMod,
		Dims: Dims{
			{ID: "mem.mod.iterator", Name: "iterator", Div: 1024},
			{ID: "mem.mod.respip", Name: "respip", Div: 1024},
			{ID: "mem.mod.validator", Name: "validator", Div: 1024},
			{ID: "mem.mod.subnet", Name: "subnet", Div: 1024},  // ifdef CLIENT_SUBNET
			{ID: "mem.mod.ipsecmod", Name: "ipsec", Div: 1024}, // ifdef USE_IPSECMOD
		},
	}
	memStreamWaitChart = Chart{
		ID:       "mem_stream_wait",
		Title:    "TCP and TLS Stream Waif Buffer Memory",
		Units:    "KB",
		Fam:      "mem",
		Ctx:      "unbound.mem_streamwait",
		Priority: prioMemStreamWait,
		Dims: Dims{
			{ID: "mem.streamwait", Name: "streamwait", Div: 1024},
		},
	}
	// NOTE: same family as for cacheChart
	cacheCountChart = Chart{
		ID:       "cache_count",
		Title:    "Cache Items Count",
		Units:    "items",
		Fam:      "cache",
		Ctx:      "unbound.cache_count",
		Type:     module.Stacked,
		Priority: prioCacheCount,
		Dims: Dims{
			{ID: "infra.cache.count", Name: "infra"},
			{ID: "key.cache.count", Name: "key"},
			{ID: "msg.cache.count", Name: "msg"},
			{ID: "rrset.cache.count", Name: "rrset"},
			{ID: "dnscrypt_nonce.cache.count", Name: "dnscrypt_nonce"},
			{ID: "dnscrypt_shared_secret.cache.count", Name: "shared_secret"},
		},
	}
	queryTypeChart = Chart{
		ID:       "queries_by_type",
		Title:    "Queries By Type",
		Units:    "queries",
		Fam:      "queries",
		Ctx:      "unbound.type_queries",
		Type:     module.Stacked,
		Priority: prioQueryType,
	}
	queryClassChart = Chart{
		ID:       "queries_by_class",
		Title:    "Queries By Class",
		Units:    "queries",
		Fam:      "queries",
		Ctx:      "unbound.class_queries",
		Type:     module.Stacked,
		Priority: prioQueryClass,
	}
	queryOpCodeChart = Chart{
		ID:       "queries_by_opcode",
		Title:    "Queries By OpCode",
		Units:    "queries",
		Fam:      "queries",
		Ctx:      "unbound.opcode_queries",
		Type:     module.Stacked,
		Priority: prioQueryOpCode,
	}
	queryFlagChart = Chart{
		ID:       "queries_by_flag",
		Title:    "Queries By Flag",
		Units:    "queries",
		Fam:      "queries",
		Ctx:      "unbound.flag_queries",
		Type:     module.Stacked,
		Priority: prioQueryFlag,
		Dims: Dims{
			{ID: "num.query.flags.QR", Name: "QR"},
			{ID: "num.query.flags.AA", Name: "AA"},
			{ID: "num.query.flags.TC", Name: "TC"},
			{ID: "num.query.flags.RD", Name: "RD"},
			{ID: "num.query.flags.RA", Name: "RA"},
			{ID: "num.query.flags.Z", Name: "Z"},
			{ID: "num.query.flags.AD", Name: "AD"},
			{ID: "num.query.flags.CD", Name: "CD"},
		},
	}
	answerRCodeChart = Chart{
		ID:       "replies_by_rcode",
		Title:    "Replies By RCode",
		Units:    "replies",
		Fam:      "replies",
		Ctx:      "unbound.rcode_answers",
		Type:     module.Stacked,
		Priority: prioReplyRCode,
	}
)

func (u *Unbound) updateCharts() {
	if len(u.curCache.threads) > 1 {
		for v := range u.curCache.threads {
			if !u.cache.threads[v] {
				u.cache.threads[v] = true
				u.addThreadCharts(v)
			}
		}
	}
	// 0-6 rcodes always included
	if hasExtendedData := len(u.curCache.answerRCode) > 0; !hasExtendedData {
		return
	}

	if !u.extChartsCreated {
		charts := extendedCharts(u.Cumulative)
		if err := u.Charts().Add(*charts...); err != nil {
			u.Warningf("add extended charts: %v", err)
		}
		u.extChartsCreated = true
	}

	for v := range u.curCache.queryType {
		if !u.cache.queryType[v] {
			u.cache.queryType[v] = true
			u.addDimToQueryTypeChart(v)
		}
	}
	for v := range u.curCache.queryClass {
		if !u.cache.queryClass[v] {
			u.cache.queryClass[v] = true
			u.addDimToQueryClassChart(v)
		}
	}
	for v := range u.curCache.queryOpCode {
		if !u.cache.queryOpCode[v] {
			u.cache.queryOpCode[v] = true
			u.addDimToQueryOpCodeChart(v)
		}
	}
	for v := range u.curCache.answerRCode {
		if !u.cache.answerRCode[v] {
			u.cache.answerRCode[v] = true
			u.addDimToAnswerRcodeChart(v)
		}
	}
}

func (u *Unbound) addThreadCharts(thread string) {
	charts := threadCharts(thread, u.Cumulative)
	if err := u.Charts().Add(*charts...); err != nil {
		u.Warningf("add '%s' charts: %v", thread, err)
	}
}

func (u *Unbound) addDimToQueryTypeChart(typ string) {
	u.addDimToChart(queryTypeChart.ID, "num.query.type."+typ, typ)
}
func (u *Unbound) addDimToQueryClassChart(class string) {
	u.addDimToChart(queryClassChart.ID, "num.query.class."+class, class)
}
func (u *Unbound) addDimToQueryOpCodeChart(opcode string) {
	u.addDimToChart(queryOpCodeChart.ID, "num.query.opcode."+opcode, opcode)
}
func (u *Unbound) addDimToAnswerRcodeChart(rcode string) {
	u.addDimToChart(answerRCodeChart.ID, "num.answer.rcode."+rcode, rcode)
}

func (u *Unbound) addDimToChart(chartID, dimID, dimName string) {
	chart := u.Charts().Get(chartID)
	if chart == nil {
		u.Warningf("add '%s' dim: couldn't find '%s' chart", dimID, chartID)
		return
	}
	dim := &Dim{ID: dimID, Name: dimName}
	if u.Cumulative {
		dim.Algo = module.Incremental
	}
	if err := chart.AddDim(dim); err != nil {
		u.Warningf("add '%s' dim: %v", dimID, err)
		return
	}
	chart.MarkNotCreated()
}

func makeIncrIf(chart *Chart, do bool) *Chart {
	if !do {
		return chart
	}
	chart.Units += "/s"
	for _, d := range chart.Dims {
		d.Algo = module.Incremental
	}
	return chart
}

func makePercOfIncrIf(chart *Chart, do bool) *Chart {
	if !do {
		return chart
	}
	for _, d := range chart.Dims {
		d.Algo = module.PercentOfIncremental
	}
	return chart
}
