package input

import (
	"context"
	"errors"
	"fmt"
	"maps"
	"sort"
	"strconv"
	"strings"
	"time"

	"github.com/gopcua/opcua/id"
	"github.com/gopcua/opcua/ua"

	"github.com/influxdata/telegraf"
	"github.com/influxdata/telegraf/config"
	"github.com/influxdata/telegraf/filter"
	"github.com/influxdata/telegraf/metric"
	"github.com/influxdata/telegraf/plugins/common/opcua"
)

type Trigger string

const (
	Status               Trigger = "Status"
	StatusValue          Trigger = "StatusValue"
	StatusValueTimestamp Trigger = "StatusValueTimestamp"
)

type DeadbandType string

const (
	None     DeadbandType = "None"
	Absolute DeadbandType = "Absolute"
	Percent  DeadbandType = "Percent"
)

type DataChangeFilter struct {
	Trigger       Trigger      `toml:"trigger"`
	DeadbandType  DeadbandType `toml:"deadband_type"`
	DeadbandValue *float64     `toml:"deadband_value"`
}

type MonitoringParameters struct {
	SamplingInterval config.Duration   `toml:"sampling_interval"`
	QueueSize        *uint32           `toml:"queue_size"`
	DiscardOldest    *bool             `toml:"discard_oldest"`
	DataChangeFilter *DataChangeFilter `toml:"data_change_filter"`
}

// NodeSettings describes how to map from a OPC UA node to a Metric
type NodeSettings struct {
	FieldName        string               `toml:"name"`
	NodeIDStr        string               `toml:"id"`
	Namespace        string               `toml:"namespace"`
	NamespaceURI     string               `toml:"namespace_uri"`
	IdentifierType   string               `toml:"identifier_type"`
	Identifier       string               `toml:"identifier"`
	DefaultTags      map[string]string    `toml:"default_tags"`
	MonitoringParams MonitoringParameters `toml:"monitoring_params"`
}

// NodeID returns the OPC UA node id
func (tag *NodeSettings) NodeID() string {
	if tag.NodeIDStr != "" {
		return tag.NodeIDStr
	}
	if tag.NamespaceURI != "" {
		return "nsu=" + tag.NamespaceURI + ";" + tag.IdentifierType + "=" + tag.Identifier
	}
	return "ns=" + tag.Namespace + ";" + tag.IdentifierType + "=" + tag.Identifier
}

// NodeGroupSettings describes a mapping of group of nodes to Metrics
type NodeGroupSettings struct {
	MetricName       string            `toml:"name"`            // Overrides plugin's setting
	Namespace        string            `toml:"namespace"`       // Can be overridden by node setting
	NamespaceURI     string            `toml:"namespace_uri"`   // Can be overridden by node setting
	IdentifierType   string            `toml:"identifier_type"` // Can be overridden by node setting
	Nodes            []NodeSettings    `toml:"nodes"`
	DefaultTags      map[string]string `toml:"default_tags"`
	SamplingInterval config.Duration   `toml:"sampling_interval"` // Can be overridden by monitoring parameters
}

type EventNodeSettings struct {
	NodeIDStr      string `toml:"id"`
	Namespace      string `toml:"namespace"`
	NamespaceURI   string `toml:"namespace_uri"`
	IdentifierType string `toml:"identifier_type"`
	Identifier     string `toml:"identifier"`
}

func (e *EventNodeSettings) NodeID() string {
	if e.NodeIDStr != "" {
		return e.NodeIDStr
	}
	if e.NamespaceURI != "" {
		return "nsu=" + e.NamespaceURI + ";" + e.IdentifierType + "=" + e.Identifier
	}
	return "ns=" + e.Namespace + ";" + e.IdentifierType + "=" + e.Identifier
}

type EventGroupSettings struct {
	SamplingInterval config.Duration     `toml:"sampling_interval"`
	QueueSize        uint32              `toml:"queue_size"`
	EventTypeNode    EventNodeSettings   `toml:"event_type_node"`
	Namespace        string              `toml:"namespace"`
	NamespaceURI     string              `toml:"namespace_uri"`
	IdentifierType   string              `toml:"identifier_type"`
	NodeIDSettings   []EventNodeSettings `toml:"node_ids"`
	SourceNames      []string            `toml:"source_names"`
	Fields           []string            `toml:"fields"`
}

func (e *EventGroupSettings) UpdateNodeIDSettings() {
	for i := range e.NodeIDSettings {
		n := &e.NodeIDSettings[i]

		// Skip group defaults when node ID string is specified directly
		if n.NodeIDStr != "" {
			continue
		}

		// Apply group defaults only if not already set
		if n.Namespace == "" {
			n.Namespace = e.Namespace
		}
		if n.NamespaceURI == "" {
			n.NamespaceURI = e.NamespaceURI
		}
		if n.IdentifierType == "" {
			n.IdentifierType = e.IdentifierType
		}
	}
}

func (e *EventGroupSettings) Validate() error {
	if err := e.EventTypeNode.validateEventNodeSettings(); err != nil {
		return fmt.Errorf("invalid event_type_node_settings: %w", err)
	}

	if len(e.NodeIDSettings) == 0 {
		return errors.New("at least one node_id must be specified")
	}

	for _, node := range e.NodeIDSettings {
		if err := node.validateEventNodeSettings(); err != nil {
			return fmt.Errorf("invalid node_id_settings: %w", err)
		}
	}

	if len(e.Fields) == 0 {
		return errors.New("at least one Field must be specified")
	}
	for _, field := range e.Fields {
		if field == "" {
			return errors.New("empty field name in fields stanza")
		}
	}
	return nil
}

func (e EventNodeSettings) validateEventNodeSettings() error {
	var defaultNodeSettings EventNodeSettings
	if e == defaultNodeSettings {
		return errors.New("node settings can't be empty")
	}

	if e.NodeIDStr != "" {
		if e.Namespace != "" || e.NamespaceURI != "" || e.IdentifierType != "" || e.Identifier != "" {
			return errors.New("cannot specify both 'id' and individual fields (namespace/namespace_uri/identifier_type/identifier)")
		}
		return nil
	}

	if e.Identifier == "" {
		return errors.New("identifier must be set")
	}
	if e.IdentifierType == "" {
		return errors.New("identifier_type must be set")
	}

	// Validate namespace configuration
	hasNamespace := len(e.Namespace) > 0
	hasNamespaceURI := len(e.NamespaceURI) > 0

	if hasNamespace && hasNamespaceURI {
		return errors.New("cannot specify both 'namespace' and 'namespace_uri', use only one")
	}

	if !hasNamespace && !hasNamespaceURI {
		return errors.New("must specify either 'namespace' or 'namespace_uri'")
	}

	return nil
}

type TimestampSource string

const (
	TimestampSourceServer   TimestampSource = "server"
	TimestampSourceSource   TimestampSource = "source"
	TimestampSourceTelegraf TimestampSource = "gather"
)

// BrowsePathSettings is one pattern-based discovery rule.
type BrowsePathSettings struct {
	Pattern     string            `toml:"pattern"`
	MetricName  string            `toml:"name"`
	DefaultTags map[string]string `toml:"default_tags"`

	// Internal fields
	compiled filter.Filter
}

// BrowseConfig configures address-space discovery for the input client.
// When Paths is empty, browse-based discovery is disabled and the existing
// nodes/group/events configuration is used as-is.
type BrowseConfig struct {
	Root      string               `toml:"root"`
	Depth     int                  `toml:"depth"`
	MaxNodes  int                  `toml:"max_nodes"`
	BatchSize int                  `toml:"batch_size"`
	Paths     []BrowsePathSettings `toml:"paths"`

	// Internal fields
	parsedRoot *ua.NodeID
}

// InputClientConfig a configuration for the input client
type InputClientConfig struct {
	opcua.OpcUAClientConfig
	MetricName      string               `toml:"name"`
	Timestamp       TimestampSource      `toml:"timestamp"`
	TimestampFormat string               `toml:"timestamp_format"`
	RootNodes       []NodeSettings       `toml:"nodes"`
	Groups          []NodeGroupSettings  `toml:"group"`
	EventGroups     []EventGroupSettings `toml:"events"`
	Browse          BrowseConfig         `toml:"browse"`
}

func (o *InputClientConfig) Validate() error {
	if o.MetricName == "" {
		return errors.New("metric name is empty")
	}

	switch string(o.Timestamp) {
	case "", "gather", "server", "source":
		// Valid timestamp source
	default:
		return fmt.Errorf("unknown timestamp source %q, expected one of: [\"\", \"gather\", \"server\", \"source\"]", o.Timestamp)
	}

	if o.TimestampFormat == "" {
		o.TimestampFormat = time.RFC3339Nano
	}

	if len(o.Groups) == 0 && len(o.RootNodes) == 0 && o.EventGroups == nil && len(o.Browse.Paths) == 0 {
		return errors.New("no groups, root nodes, browse paths or events provided to gather from")
	}
	for _, group := range o.Groups {
		if len(group.Nodes) == 0 {
			return errors.New("group has no nodes to collect from")
		}
	}

	if len(o.Browse.Paths) > 0 {
		if o.Browse.Root == "" {
			// OPC UA Objects folder, the standard top of the user-visible address space.
			o.Browse.Root = "ns=0;i=85"
		}
		rootID, err := ua.ParseNodeID(o.Browse.Root)
		if err != nil {
			return fmt.Errorf("invalid browse root %q: %w", o.Browse.Root, err)
		}
		o.Browse.parsedRoot = rootID
		if o.Browse.BatchSize <= 0 {
			o.Browse.BatchSize = 50
		}
		for i := range o.Browse.Paths {
			p := &o.Browse.Paths[i]
			if p.Pattern == "" {
				return fmt.Errorf("browse path at index %d has empty pattern", i)
			}
			f, err := filter.Compile([]string{p.Pattern}, '/')
			if err != nil {
				return fmt.Errorf("invalid browse pattern at index %d: %w", i, err)
			}
			p.compiled = f
		}
	}

	return nil
}

func (o *InputClientConfig) CreateInputClient(log telegraf.Logger) (*OpcUAInputClient, error) {
	if err := o.Validate(); err != nil {
		return nil, err
	}

	if o.EventGroups != nil {
		for i := range o.EventGroups {
			o.EventGroups[i].UpdateNodeIDSettings()
			if err := o.EventGroups[i].Validate(); err != nil {
				return nil, fmt.Errorf("invalid event_settings: %w", err)
			}
		}
	}

	log.Debug("Initialising OpcUAInputClient")
	opcClient, err := o.OpcUAClientConfig.CreateClient(log)
	if err != nil {
		return nil, err
	}

	c := &OpcUAInputClient{
		OpcUAClient:    opcClient,
		Log:            log,
		Config:         *o,
		EventGroups:    o.EventGroups,
		userGroupCount: len(o.Groups),
	}

	// Browse-based discovery defers metric mapping until after Connect.
	// The discovered nodes are not known until the server is reachable.
	if len(o.Browse.Paths) == 0 {
		log.Debug("Initialising node to metric mapping")
		if err := c.InitNodeMetricMapping(); err != nil {
			return nil, err
		}
	}

	return c, nil
}

// NodeMetricMapping mapping from a single node to a metric
type NodeMetricMapping struct {
	Tag        NodeSettings
	idStr      string
	metricName string
	MetricTags map[string]string
}

// NewNodeMetricMapping builds a new NodeMetricMapping from the given argument
func NewNodeMetricMapping(metricName string, node NodeSettings, groupTags map[string]string) (*NodeMetricMapping, error) {
	mergedTags := make(map[string]string)
	maps.Copy(mergedTags, groupTags)
	maps.Copy(mergedTags, node.DefaultTags)

	return &NodeMetricMapping{
		Tag:        node,
		idStr:      node.NodeID(),
		metricName: metricName,
		MetricTags: mergedTags,
	}, nil
}

type EventNodeMetricMapping struct {
	NodeID           *ua.NodeID
	SamplingInterval *config.Duration
	QueueSize        *uint32
	EventTypeNode    *ua.NodeID
	SourceNames      []string
	Fields           []string
}

// NodeValue The received value for a node
type NodeValue struct {
	TagName    string
	Value      interface{}
	Quality    ua.StatusCode
	ServerTime time.Time
	SourceTime time.Time
	DataType   ua.TypeID
	IsArray    bool
}

// OpcUAInputClient can receive data from an OPC UA server and map it to Metrics. This type does not contain
// logic for actually retrieving data from the server, but is used by other types like ReadClient and
// OpcUAInputSubscribeClient to store data needed to convert node ids to the corresponding metrics.
type OpcUAInputClient struct {
	*opcua.OpcUAClient
	Config InputClientConfig
	Log    telegraf.Logger

	NodeMetricMapping      []NodeMetricMapping
	NodeIDs                []*ua.NodeID
	LastReceivedData       []NodeValue
	EventGroups            []EventGroupSettings
	EventNodeMetricMapping []EventNodeMetricMapping

	// Internal fields
	userGroupCount int
}

// DiscoverNodes walks the address space using the configured browse settings
// and replaces any previously discovered node groups on the client's
// configuration with the freshly resolved ones. User-supplied groups (those
// present before any discovery) are preserved. Safe to call repeatedly across
// reconnects so dynamically added or removed server nodes are picked up.
// Browse failures bubble up as errors, but patterns that match no nodes only
// produce a log entry so partial-server misconfiguration does not block
// collection from explicit nodes.
func (o *OpcUAInputClient) DiscoverNodes(ctx context.Context) error {
	browser := &opcua.AddressSpaceBrowser{
		Client:    o.Client,
		Log:       o.Log,
		MaxDepth:  o.Config.Browse.Depth,
		MaxNodes:  o.Config.Browse.MaxNodes,
		BatchSize: o.Config.Browse.BatchSize,
	}
	nodes, err := browser.Browse(ctx, o.Config.Browse.parsedRoot)
	if err != nil {
		return fmt.Errorf("browsing address space failed: %w", err)
	}
	o.Log.Infof("Browse discovered %d nodes from root %q", len(nodes), o.Config.Browse.Root)

	groups, matched := ResolveBrowsedNodes(nodes, o.Config.Browse.Paths)
	o.Log.Infof("Browse patterns matched %d variables", matched)

	// Drop the previously discovered groups before re-appending, so the
	// effective Groups slice stays bounded across reconnects.
	o.Config.Groups = append(o.Config.Groups[:o.userGroupCount], groups...)
	return nil
}

// Stop the connection to the client
func (o *OpcUAInputClient) Stop(ctx context.Context) <-chan struct{} {
	ch := make(chan struct{})
	defer close(ch)
	err := o.Disconnect(ctx)
	if err != nil {
		o.Log.Warn("Disconnecting from server failed with error ", err)
	}

	return ch
}

// metricParts is only used to ensure no duplicate metrics are created
type metricParts struct {
	metricName string
	fieldName  string
	tags       string // sorted by tag name and in format tag1=value1, tag2=value2
}

func newMP(n *NodeMetricMapping) metricParts {
	// Include the node ID as the "id" tag since MetricForNode always adds it
	tags := map[string]string{"id": n.idStr}
	for k, v := range n.MetricTags {
		tags[k] = v
	}
	keys := make([]string, 0, len(tags))
	for key := range tags {
		keys = append(keys, key)
	}
	sort.Strings(keys)
	var sb strings.Builder
	for i, key := range keys {
		if i != 0 {
			sb.WriteString(", ")
		}
		sb.WriteString(key)
		sb.WriteString("=")
		sb.WriteString(tags[key])
	}
	return metricParts{
		metricName: n.metricName,
		fieldName:  n.Tag.FieldName,
		tags:       sb.String(),
	}
}

func validateNodeToAdd(existing map[metricParts]struct{}, nmm *NodeMetricMapping) error {
	if nmm.Tag.FieldName == "" {
		return fmt.Errorf("empty name in %q", nmm.Tag.FieldName)
	}

	if nmm.Tag.NodeIDStr != "" {
		if nmm.Tag.Namespace != "" || nmm.Tag.NamespaceURI != "" || nmm.Tag.IdentifierType != "" || nmm.Tag.Identifier != "" {
			return fmt.Errorf("node %q: cannot specify both 'id' and individual fields (namespace/namespace_uri/identifier_type/identifier)", nmm.Tag.FieldName)
		}
	} else {
		// Validate namespace configuration
		hasNamespace := len(nmm.Tag.Namespace) > 0
		hasNamespaceURI := len(nmm.Tag.NamespaceURI) > 0

		if hasNamespace && hasNamespaceURI {
			return fmt.Errorf("node %q: cannot specify both 'namespace' and 'namespace_uri', use only one", nmm.Tag.FieldName)
		}

		if !hasNamespace && !hasNamespaceURI {
			return fmt.Errorf("node %q: must specify either 'namespace' or 'namespace_uri'", nmm.Tag.FieldName)
		}

		if len(nmm.Tag.Identifier) == 0 {
			return errors.New("empty node identifier not allowed")
		}

		switch nmm.Tag.IdentifierType {
		case "i":
			if _, err := strconv.Atoi(nmm.Tag.Identifier); err != nil {
				return fmt.Errorf("identifier type %q does not match the type of identifier %q", nmm.Tag.IdentifierType, nmm.Tag.Identifier)
			}
		case "s", "g", "b":
			// Valid identifier type - do nothing.
		default:
			return fmt.Errorf("invalid identifier type %q in %q", nmm.Tag.IdentifierType, nmm.Tag.FieldName)
		}
	}

	for k, v := range nmm.MetricTags {
		if k == "" {
			return fmt.Errorf("empty tag name in tags for %q", nmm.Tag.FieldName)
		}
		if v == "" {
			return fmt.Errorf("empty tag value for tag %q in %q", k, nmm.Tag.FieldName)
		}
	}

	mp := newMP(nmm)
	if _, exists := existing[mp]; exists {
		return fmt.Errorf("name %q is duplicated (metric name %q, tags %q)",
			mp.fieldName, mp.metricName, mp.tags)
	}

	existing[mp] = struct{}{}
	return nil
}

// InitNodeMetricMapping builds nodes from the configuration. Safe to call
// repeatedly: any previous mappings are discarded so the result reflects the
// current Config.RootNodes and Config.Groups.
func (o *OpcUAInputClient) InitNodeMetricMapping() error {
	o.NodeMetricMapping = nil
	existing := make(map[metricParts]struct{}, len(o.Config.RootNodes))
	for _, node := range o.Config.RootNodes {
		nmm, err := NewNodeMetricMapping(o.Config.MetricName, node, make(map[string]string))
		if err != nil {
			return err
		}

		if err := validateNodeToAdd(existing, nmm); err != nil {
			return err
		}
		o.NodeMetricMapping = append(o.NodeMetricMapping, *nmm)
	}

	for gi := range o.Config.Groups {
		group := &o.Config.Groups[gi]
		if group.MetricName == "" {
			group.MetricName = o.Config.MetricName
		}

		for _, node := range group.Nodes {
			// Skip group defaults when node ID string is specified directly
			if node.NodeIDStr == "" {
				if node.Namespace == "" {
					node.Namespace = group.Namespace
				}
				if node.NamespaceURI == "" {
					node.NamespaceURI = group.NamespaceURI
				}
				if node.IdentifierType == "" {
					node.IdentifierType = group.IdentifierType
				}
			}
			if node.MonitoringParams.SamplingInterval == 0 {
				node.MonitoringParams.SamplingInterval = group.SamplingInterval
			}

			nmm, err := NewNodeMetricMapping(group.MetricName, node, group.DefaultTags)
			if err != nil {
				return err
			}

			if err := validateNodeToAdd(existing, nmm); err != nil {
				return err
			}
			o.NodeMetricMapping = append(o.NodeMetricMapping, *nmm)
		}
	}

	o.initLastReceivedValues()
	return nil
}

func (o *OpcUAInputClient) InitNodeIDs() error {
	// Get all namespace definitions of the remote device for handling
	// namespace URLs (the ones starting with "nsu=") gracefully
	namespaces := o.NamespaceArray()

	o.NodeIDs = make([]*ua.NodeID, 0, len(o.NodeMetricMapping))
	for _, node := range o.NodeMetricMapping {
		// Determine the actual node-ID instance using the expanded form as
		// this handles namespace URLs (nsu=) gracefully
		nodeID := node.Tag.NodeID()
		expanded, err := ua.ParseExpandedNodeID(nodeID, namespaces)
		if err != nil {
			return fmt.Errorf("failed to parse node ID %q: %w", nodeID, err)
		}
		o.NodeIDs = append(o.NodeIDs, ua.NewNodeIDFromExpandedNodeID(expanded))
	}

	return nil
}

func (o *OpcUAInputClient) InitEventNodeIDs() error {
	// Get all namespace definitions of the remote device for handling
	// namespace URLs (the ones starting with "nsu=") gracefully
	namespaces := o.NamespaceArray()

	for _, eventSetting := range o.EventGroups {
		// Parse event type node ID
		eventNodeID := eventSetting.EventTypeNode.NodeID()
		expanded, err := ua.ParseExpandedNodeID(eventNodeID, namespaces)
		if err != nil {
			return fmt.Errorf("failed to parse event type node ID %q: %w", eventNodeID, err)
		}
		eid := ua.NewNodeIDFromExpandedNodeID(expanded)

		for _, node := range eventSetting.NodeIDSettings {
			nodeID := node.NodeID()
			expanded, err := ua.ParseExpandedNodeID(nodeID, namespaces)
			if err != nil {
				return fmt.Errorf("failed to parse node ID %q: %w", nodeID, err)
			}

			nmm := EventNodeMetricMapping{
				NodeID:           ua.NewNodeIDFromExpandedNodeID(expanded),
				SamplingInterval: &eventSetting.SamplingInterval,
				QueueSize:        &eventSetting.QueueSize,
				EventTypeNode:    eid,
				SourceNames:      eventSetting.SourceNames,
				Fields:           eventSetting.Fields,
			}
			o.EventNodeMetricMapping = append(o.EventNodeMetricMapping, nmm)
		}
	}

	return nil
}

func (o *OpcUAInputClient) initLastReceivedValues() {
	o.LastReceivedData = make([]NodeValue, len(o.NodeMetricMapping))
	for nodeIdx, nmm := range o.NodeMetricMapping {
		o.LastReceivedData[nodeIdx].TagName = nmm.Tag.FieldName
	}
}

func (o *OpcUAInputClient) UpdateNodeValue(nodeIdx int, d *ua.DataValue) {
	o.LastReceivedData[nodeIdx].Quality = d.Status
	if !o.StatusCodeOK(d.Status) {
		// Verify NodeIDs array has been built before trying to get item; otherwise show '?' for node id
		if len(o.NodeIDs) > nodeIdx {
			o.Log.Errorf("status not OK for node %v (%v): %v", o.NodeMetricMapping[nodeIdx].Tag.FieldName, o.NodeIDs[nodeIdx].String(), d.Status)
		} else {
			o.Log.Errorf("status not OK for node %v (%v): %v", o.NodeMetricMapping[nodeIdx].Tag.FieldName, '?', d.Status)
		}

		return
	}

	if d.Value != nil {
		o.LastReceivedData[nodeIdx].DataType = d.Value.Type()
		o.LastReceivedData[nodeIdx].IsArray = d.Value.Has(ua.VariantArrayValues)

		o.LastReceivedData[nodeIdx].Value = d.Value.Value()
		if o.LastReceivedData[nodeIdx].DataType == ua.TypeIDDateTime {
			if t, ok := d.Value.Value().(time.Time); ok {
				o.LastReceivedData[nodeIdx].Value = t.Format(o.Config.TimestampFormat)
			}
		}
	}
	o.LastReceivedData[nodeIdx].ServerTime = d.ServerTimestamp
	o.LastReceivedData[nodeIdx].SourceTime = d.SourceTimestamp
}

func (o *OpcUAInputClient) MetricForNode(nodeIdx int) telegraf.Metric {
	nmm := &o.NodeMetricMapping[nodeIdx]
	tags := map[string]string{
		"id": nmm.idStr,
	}
	for k, v := range nmm.MetricTags {
		tags[k] = v
	}

	fields := make(map[string]interface{})
	if o.LastReceivedData[nodeIdx].Value != nil {
		// Simple scalar types can be stored directly under the field name while
		// arrays (see 5.2.5) and structures (see 5.2.6) must be unpacked.
		// Note: Structures and arrays of structures are currently not supported.
		if o.LastReceivedData[nodeIdx].IsArray {
			switch typedValue := o.LastReceivedData[nodeIdx].Value.(type) {
			case []uint8:
				fields = unpack(nmm.Tag.FieldName, typedValue)
			case ua.ByteArray:
				fields = unpack(nmm.Tag.FieldName, typedValue)
			case []uint16:
				fields = unpack(nmm.Tag.FieldName, typedValue)
			case []uint32:
				fields = unpack(nmm.Tag.FieldName, typedValue)
			case []uint64:
				fields = unpack(nmm.Tag.FieldName, typedValue)
			case []int8:
				fields = unpack(nmm.Tag.FieldName, typedValue)
			case []int16:
				fields = unpack(nmm.Tag.FieldName, typedValue)
			case []int32:
				fields = unpack(nmm.Tag.FieldName, typedValue)
			case []int64:
				fields = unpack(nmm.Tag.FieldName, typedValue)
			case []float32:
				fields = unpack(nmm.Tag.FieldName, typedValue)
			case []float64:
				fields = unpack(nmm.Tag.FieldName, typedValue)
			case []string:
				fields = unpack(nmm.Tag.FieldName, typedValue)
			case []bool:
				fields = unpack(nmm.Tag.FieldName, typedValue)
			case []time.Time:
				strs := make([]string, len(typedValue))
				for i, t := range typedValue {
					strs[i] = t.Format(o.Config.TimestampFormat)
				}
				fields = unpack(nmm.Tag.FieldName, strs)
			default:
				o.Log.Errorf("could not unpack variant array of type: %T", typedValue)
			}
		} else {
			fields = map[string]interface{}{
				nmm.Tag.FieldName: o.LastReceivedData[nodeIdx].Value,
			}
		}
	}

	fields["Quality"] = strings.TrimSpace(o.LastReceivedData[nodeIdx].Quality.Error())
	for _, field := range o.Config.OptionalFields {
		if field == "DataType" {
			fields["DataType"] = strings.Replace(o.LastReceivedData[nodeIdx].DataType.String(), "TypeID", "", 1)
			break
		}
	}
	if !o.StatusCodeOK(o.LastReceivedData[nodeIdx].Quality) {
		mp := newMP(nmm)
		o.Log.Debugf("status not OK for node %q(metric name %q, tags %q)",
			mp.fieldName, mp.metricName, mp.tags)
	}

	var t time.Time
	switch o.Config.Timestamp {
	case TimestampSourceServer:
		t = o.LastReceivedData[nodeIdx].ServerTime
	case TimestampSourceSource:
		t = o.LastReceivedData[nodeIdx].SourceTime
	default:
		t = time.Now()
	}

	return metric.New(nmm.metricName, tags, fields, t)
}

func unpack[Slice ~[]E, E any](prefix string, value Slice) map[string]interface{} {
	fields := make(map[string]interface{}, len(value))
	for i, v := range value {
		key := fmt.Sprintf("%s[%d]", prefix, i)
		fields[key] = v
	}
	return fields
}

func (o *OpcUAInputClient) MetricForEvent(nodeIdx int, event *ua.EventFieldList) telegraf.Metric {
	node := o.EventNodeMetricMapping[nodeIdx]
	fields := make(map[string]interface{}, len(event.EventFields))
	var sourceTime, serverTime time.Time
	for i, field := range event.EventFields {
		name := node.Fields[i]
		value := field.Value()

		if value == nil {
			o.Log.Warnf("Field %s has no value", name)
			continue
		}

		switch v := value.(type) {
		case *ua.LocalizedText:
			fields[name] = v.Text
		case time.Time:
			if name == "Time" {
				sourceTime = v
			} else if name == "ReceiveTime" {
				serverTime = v
			}
			fields[name] = v.Format(time.RFC3339)
		default:
			fields[name] = v
		}
	}

	if len(fields) == 0 {
		o.Log.Warn("Event has no fields with values, skipping")
		return nil
	}

	tags := map[string]string{
		"node_id": node.NodeID.String(),
		"source":  o.Config.Endpoint,
	}
	var t time.Time
	switch o.Config.Timestamp {
	case TimestampSourceServer:
		t = serverTime
	case TimestampSourceSource:
		t = sourceTime
	}
	if t.IsZero() {
		t = time.Now()
	}

	return metric.New("opcua_event", tags, fields, t)
}

// CreateEventFilter creates a new event filter for event streaming
func (node *EventNodeMetricMapping) CreateEventFilter() (*ua.ExtensionObject, error) {
	selects, err := node.createSelectClauses()
	if err != nil {
		return nil, err
	}
	wheres, err := node.createWhereClauses()
	if err != nil {
		return nil, err
	}
	return &ua.ExtensionObject{
		EncodingMask: ua.ExtensionObjectBinary,
		TypeID:       &ua.ExpandedNodeID{NodeID: ua.NewNumericNodeID(0, id.EventFilter_Encoding_DefaultBinary)},
		Value: ua.EventFilter{
			SelectClauses: selects,
			WhereClause:   wheres,
		},
	}, nil
}

func (node *EventNodeMetricMapping) createSelectClauses() ([]*ua.SimpleAttributeOperand, error) {
	selects := make([]*ua.SimpleAttributeOperand, len(node.Fields))
	typeDefinition, err := node.determineNodeIDType()
	if err != nil {
		return nil, err
	}
	for i, name := range node.Fields {
		browsePath, err := parseBrowsePath(name)
		if err != nil {
			return nil, fmt.Errorf("parsing field %q failed: %w", name, err)
		}
		selects[i] = &ua.SimpleAttributeOperand{
			TypeDefinitionID: typeDefinition,
			BrowsePath:       browsePath,
			AttributeID:      ua.AttributeIDValue,
		}
	}
	return selects, nil
}

// parseBrowsePath parses a field name into a browse path of qualified names.
// It supports namespace-qualified segments (e.g. "2:TEXT01") and multi-segment
// paths separated by "/" (e.g. "AckedState/Id" or "2:AckedState/0:Id").
func parseBrowsePath(field string) ([]*ua.QualifiedName, error) {
	segments := strings.Split(field, "/")
	path := make([]*ua.QualifiedName, 0, len(segments))
	for _, seg := range segments {
		if seg == "" {
			return nil, fmt.Errorf("empty segment in browse path %q", field)
		}
		ns, name := parseQualifiedName(seg)
		path = append(path, &ua.QualifiedName{NamespaceIndex: ns, Name: name})
	}
	return path, nil
}

// parseQualifiedName parses a single segment like "2:TEXT01" into a namespace
// index and name. If no namespace prefix is present, namespace 0 is used.
func parseQualifiedName(segment string) (uint16, string) {
	prefix, name, found := strings.Cut(segment, ":")
	if !found {
		return 0, segment
	}
	ns, err := strconv.ParseUint(prefix, 10, 16)
	if err != nil {
		// Not a valid namespace prefix, treat the whole segment as the name
		return 0, segment
	}
	return uint16(ns), name
}

func (node *EventNodeMetricMapping) createWhereClauses() (*ua.ContentFilter, error) {
	if len(node.SourceNames) == 0 {
		return &ua.ContentFilter{
			Elements: make([]*ua.ContentFilterElement, 0),
		}, nil
	}
	operands := make([]*ua.ExtensionObject, 0, len(node.SourceNames))
	for _, sourceName := range node.SourceNames {
		literalOperand := &ua.ExtensionObject{
			EncodingMask: 1,
			TypeID: &ua.ExpandedNodeID{
				NodeID: ua.NewNumericNodeID(0, id.LiteralOperand_Encoding_DefaultBinary),
			},
			Value: ua.LiteralOperand{
				Value: ua.MustVariant(sourceName),
			},
		}
		operands = append(operands, literalOperand)
	}

	typeDefinition, err := node.determineNodeIDType()
	if err != nil {
		return nil, err
	}

	attributeOperand := &ua.ExtensionObject{
		EncodingMask: ua.ExtensionObjectBinary,
		TypeID: &ua.ExpandedNodeID{
			NodeID: ua.NewNumericNodeID(0, id.SimpleAttributeOperand_Encoding_DefaultBinary),
		},
		Value: &ua.SimpleAttributeOperand{
			TypeDefinitionID: typeDefinition,
			BrowsePath: []*ua.QualifiedName{
				{NamespaceIndex: 0, Name: "SourceName"},
			},
			AttributeID: ua.AttributeIDValue,
		},
	}

	filterElement := &ua.ContentFilterElement{
		FilterOperator: ua.FilterOperatorInList,
		FilterOperands: append([]*ua.ExtensionObject{attributeOperand}, operands...),
	}

	wheres := &ua.ContentFilter{
		Elements: []*ua.ContentFilterElement{filterElement},
	}

	return wheres, nil
}

func (node *EventNodeMetricMapping) determineNodeIDType() (*ua.NodeID, error) {
	switch node.EventTypeNode.Type() {
	case ua.NodeIDTypeGUID:
		return ua.NewGUIDNodeID(node.EventTypeNode.Namespace(), node.EventTypeNode.StringID()), nil
	case ua.NodeIDTypeString:
		return ua.NewStringNodeID(node.EventTypeNode.Namespace(), node.EventTypeNode.StringID()), nil
	case ua.NodeIDTypeByteString:
		return ua.NewByteStringNodeID(node.EventTypeNode.Namespace(), []byte(node.EventTypeNode.StringID())), nil
	case ua.NodeIDTypeTwoByte:
		nodeID := node.EventTypeNode.IntID()
		if nodeID > 255 {
			return nil, fmt.Errorf("twoByte EventType requires a value in the range 0-255, got %d", nodeID)
		}
		return ua.NewTwoByteNodeID(uint8(node.EventTypeNode.IntID())), nil
	case ua.NodeIDTypeFourByte:
		return ua.NewFourByteNodeID(uint8(node.EventTypeNode.Namespace()), uint16(node.EventTypeNode.IntID())), nil
	case ua.NodeIDTypeNumeric:
		return ua.NewNumericNodeID(node.EventTypeNode.Namespace(), node.EventTypeNode.IntID()), nil
	default:
		return nil, fmt.Errorf("unsupported NodeID type: %v", node.EventTypeNode.String())
	}
}
