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

package ddsnmpcollector

import (
	"bytes"
	"fmt"
	"maps"
	"strconv"

	"github.com/gosnmp/gosnmp"

	"github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp"
	"github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp/ddprofiledefinition"
	"github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/oldmetrix"
)

type metricBuilder struct {
	metric ddsnmp.Metric
}

func newMetricBuilder(name string, value int64) *metricBuilder {
	return &metricBuilder{
		metric: ddsnmp.Metric{
			Name:  name,
			Value: value,
		},
	}
}

func (mb *metricBuilder) withTags(tags map[string]string) *metricBuilder {
	if len(tags) > 0 {
		mb.metric.Tags = maps.Clone(tags)
	}
	return mb
}

func (mb *metricBuilder) withStaticTags(tags map[string]string) *metricBuilder {
	if len(tags) > 0 {
		mb.metric.StaticTags = maps.Clone(tags)
		if mb.metric.Tags == nil {
			mb.metric.Tags = maps.Clone(tags)
		} else {
			for k, v := range tags {
				if current, ok := mb.metric.Tags[k]; !ok || current == "" {
					mb.metric.Tags[k] = v
				}
			}
		}
	}
	return mb
}

func (mb *metricBuilder) asTableMetric(table string) *metricBuilder {
	mb.metric.IsTable = true
	mb.metric.Table = table
	return mb
}

func (mb *metricBuilder) fromSymbol(sym ddprofiledefinition.SymbolConfig, pdu gosnmp.SnmpPDU) *metricBuilder {
	mb.metric.Unit = sym.ChartMeta.Unit
	mb.metric.Description = sym.ChartMeta.Description
	mb.metric.Family = sym.ChartMeta.Family
	mb.metric.ChartType = sym.ChartMeta.Type
	mb.metric.MetricType = ternary(sym.MetricType != "", sym.MetricType, getMetricTypeFromPDUType(pdu))
	mb.metric.MultiValue = buildMultiValue(mb.metric.Value, sym.Mapping)
	return mb
}

func (mb *metricBuilder) build() ddsnmp.Metric {
	return mb.metric
}

func buildScalarMetric(cfg ddprofiledefinition.SymbolConfig, pdu gosnmp.SnmpPDU, value int64, tags, staticTags map[string]string) (*ddsnmp.Metric, error) {
	metric := newMetricBuilder(cfg.Name, value).
		withTags(tags).
		withStaticTags(staticTags).
		fromSymbol(cfg, pdu).
		build()

	if cfg.TransformCompiled != nil {
		if err := applyTransform(&metric, cfg); err != nil {
			return nil, err
		}
	}

	return &metric, nil
}

func buildTableMetric(cfg ddprofiledefinition.SymbolConfig, pdu gosnmp.SnmpPDU, value int64, tags, staticTags map[string]string, tableName string) (*ddsnmp.Metric, error) {
	metric := newMetricBuilder(cfg.Name, value).
		withTags(tags).
		withStaticTags(staticTags).
		fromSymbol(cfg, pdu).
		asTableMetric(tableName).
		build()

	if cfg.TransformCompiled != nil {
		if err := applyTransform(&metric, cfg); err != nil {
			return nil, err
		}
	}

	return &metric, nil
}

func buildMultiValue(value int64, mapping ddprofiledefinition.MappingConfig) map[string]int64 {
	if !mapping.HasItems() {
		return nil
	}

	if mapping.EffectiveMode() == ddprofiledefinition.MappingModeBitmask {
		return buildBitmaskMultiValue(value, mapping.Items)
	}

	// Check if this is an int→int mapping (value transformation)
	if isIntToIntMapping := func() bool {
		for k, v := range mapping.Items {
			if !isInt(k) || !isInt(v) {
				return false
			}
		}
		return true
	}(); isIntToIntMapping {
		return nil
	}

	isMappingKeysNumeric := func() bool {
		for k := range mapping.Items {
			if !isInt(k) {
				return false
			}
		}
		return true
	}()

	multiValue := make(map[string]int64)

	if isMappingKeysNumeric {
		// int→string mapping (e.g., 1→"up", 2→"down")
		for k, v := range mapping.Items {
			intKey, _ := strconv.ParseInt(k, 10, 64)
			// Only set the value if:
			// 1. We haven't seen this state name before (!ok), OR
			// 2. The current value matches this key (value == intKey)
			// This ensures that if multiple keys map to the same state name,
			// we preserve the "1" (active) value if any key matches
			if _, ok := multiValue[v]; !ok || value == intKey {
				multiValue[v] = oldmetrix.Bool(value == intKey)
			}
		}
	} else {
		// string→int mapping (e.g., "OK"→"0", "WARNING"→"1", "CRITICAL"→"2")
		// value has already been converted from string to int by the value processor
		// We need to find which original string maps to our current value
		for k, v := range mapping.Items {
			if intVal, err := strconv.ParseInt(v, 10, 64); err == nil {
				multiValue[k] = oldmetrix.Bool(value == intVal)
			}
		}
	}

	return multiValue
}

func buildBitmaskMultiValue(value int64, mappings map[string]string) map[string]int64 {
	multiValue := make(map[string]int64)

	for k, v := range mappings {
		bit, err := strconv.ParseInt(k, 10, 64)
		if err != nil {
			continue
		}

		active := false
		// Bitmask mappings are intended for non-negative flag values. If multiple
		// bits map to the same dimension, we OR them by keeping the active state.
		switch {
		case bit == 0:
			active = value == 0
		default:
			active = value&bit == bit
		}

		if _, ok := multiValue[v]; !ok || active {
			multiValue[v] = oldmetrix.Bool(active)
		}
	}

	return multiValue
}

func applyTransform(metric *ddsnmp.Metric, sym ddprofiledefinition.SymbolConfig) error {
	if sym.TransformCompiled == nil {
		return nil
	}

	ctx := struct{ Metric *ddsnmp.Metric }{Metric: metric}

	var buf bytes.Buffer
	if err := sym.TransformCompiled.Execute(&buf, ctx); err != nil {
		return fmt.Errorf("failed to execute transform template: %w", err)
	}

	return nil
}
