package opcua_listener

import (
	"context"
	"errors"
	"fmt"
	"reflect"
	"time"

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

	"github.com/influxdata/telegraf"
	"github.com/influxdata/telegraf/config"
	opcuaclient "github.com/influxdata/telegraf/plugins/common/opcua"
	"github.com/influxdata/telegraf/plugins/common/opcua/input"
)

type subscribeClientConfig struct {
	input.InputClientConfig
	SubscriptionInterval config.Duration `toml:"subscription_interval"`
	ConnectFailBehavior  string          `toml:"connect_fail_behavior"`
}

type subscribeClient struct {
	*input.OpcUAInputClient
	Config subscribeClientConfig

	sub                *opcua.Subscription
	monitoredItemsReqs []*ua.MonitoredItemCreateRequest
	eventItemsReqs     []*ua.MonitoredItemCreateRequest
	dataNotifications  chan *opcua.PublishNotificationData
	metrics            chan telegraf.Metric

	ctx    context.Context
	cancel context.CancelFunc
}

func checkDataChangeFilterParameters(params *input.DataChangeFilter) error {
	switch {
	case params.Trigger != input.Status &&
		params.Trigger != input.StatusValue &&
		params.Trigger != input.StatusValueTimestamp:
		return fmt.Errorf("trigger '%s' not supported", params.Trigger)
	case params.DeadbandType != input.None &&
		params.DeadbandType != input.Absolute &&
		params.DeadbandType != input.Percent:
		return fmt.Errorf("deadband_type '%s' not supported", params.DeadbandType)
	case params.DeadbandType != input.None && params.DeadbandValue == nil:
		return errors.New("deadband_value was not set")
	case params.DeadbandValue != nil && *params.DeadbandValue < 0:
		return errors.New("negative deadband_value not supported")
	default:
		return nil
	}
}

func assignConfigValuesToRequest(req *ua.MonitoredItemCreateRequest, monParams *input.MonitoringParameters) error {
	req.RequestedParameters.SamplingInterval = float64(time.Duration(monParams.SamplingInterval) / time.Millisecond)

	if monParams.QueueSize != nil {
		req.RequestedParameters.QueueSize = *monParams.QueueSize
	}

	if monParams.DiscardOldest != nil {
		req.RequestedParameters.DiscardOldest = *monParams.DiscardOldest
	}

	if monParams.DataChangeFilter != nil {
		if err := checkDataChangeFilterParameters(monParams.DataChangeFilter); err != nil {
			return fmt.Errorf("node '%s': %w", req.ItemToMonitor.NodeID, err)
		}

		var deadbandValue float64

		if monParams.DataChangeFilter.DeadbandValue != nil {
			deadbandValue = *monParams.DataChangeFilter.DeadbandValue
		}

		req.RequestedParameters.Filter = ua.NewExtensionObject(
			&ua.DataChangeFilter{
				Trigger:       ua.DataChangeTriggerFromString(string(monParams.DataChangeFilter.Trigger)),
				DeadbandType:  uint32(ua.DeadbandTypeFromString(string(monParams.DataChangeFilter.DeadbandType))),
				DeadbandValue: deadbandValue,
			},
		)
	}

	return nil
}

func (sc *subscribeClientConfig) createSubscribeClient(log telegraf.Logger) (*subscribeClient, error) {
	client, err := sc.InputClientConfig.CreateInputClient(log)
	if err != nil {
		return nil, err
	}

	// Validate monitoring parameters at config time (no server connection needed)
	for _, node := range client.NodeMetricMapping {
		if node.Tag.MonitoringParams.DataChangeFilter != nil {
			if err := checkDataChangeFilterParameters(node.Tag.MonitoringParams.DataChangeFilter); err != nil {
				return nil, fmt.Errorf("node '%s': %w", node.Tag.NodeID(), err)
			}
		}
	}

	processingCtx, processingCancel := context.WithCancel(context.Background())

	subClient := &subscribeClient{
		OpcUAInputClient:   client,
		Config:             *sc,
		monitoredItemsReqs: make([]*ua.MonitoredItemCreateRequest, 0, len(client.NodeMetricMapping)),
		eventItemsReqs:     make([]*ua.MonitoredItemCreateRequest, 0, len(client.EventNodeMetricMapping)),
		// 100 was chosen to make sure that the channels will not block when multiple changes come in at the same time.
		// The channel size should be increased if reports come in on Telegraf blocking when many changes come in at
		// the same time. It could be made dependent on the number of nodes subscribed to and the subscription interval.
		dataNotifications: make(chan *opcua.PublishNotificationData, 100),
		metrics:           make(chan telegraf.Metric, 100),
		ctx:               processingCtx,
		cancel:            processingCancel,
	}

	return subClient, nil
}

func (o *subscribeClient) connect() error {
	err := o.OpcUAClient.Connect(o.ctx)
	if err != nil {
		return err
	}

	// Fetch namespace array for namespace URI support
	// This is needed if any nodes use nsu= format instead of ns= format
	if err := o.OpcUAClient.UpdateNamespaceArray(o.ctx); err != nil {
		o.Log.Warnf("Failed to fetch namespace array: %v", err)
		// Continue anyway - this is only needed if using namespace URIs
	}

	// Browse-based discovery runs on every connect so server-side schema
	// changes (added or removed nodes, renumbered namespaces) are picked up
	// on reconnect. DiscoverNodes replaces the previously discovered groups
	// and InitNodeMetricMapping rebuilds the mapping from scratch.
	if len(o.Config.Browse.Paths) > 0 {
		if err := o.OpcUAInputClient.DiscoverNodes(o.ctx); err != nil {
			return fmt.Errorf("browse discovery failed: %w", err)
		}
		if err := o.OpcUAInputClient.InitNodeMetricMapping(); err != nil {
			return fmt.Errorf("initializing node metric mapping failed: %w", err)
		}
	}

	// Initialize node IDs after connection so namespace URIs can be resolved
	if err := o.OpcUAInputClient.InitNodeIDs(); err != nil {
		return fmt.Errorf("initializing node IDs failed: %w", err)
	}
	if err := o.OpcUAInputClient.InitEventNodeIDs(); err != nil {
		return fmt.Errorf("initializing event node IDs failed: %w", err)
	}

	o.Log.Debugf("Creating monitored items")
	o.monitoredItemsReqs = make([]*ua.MonitoredItemCreateRequest, 0, len(o.NodeIDs))
	for i, nodeID := range o.NodeIDs {
		req := opcua.NewMonitoredItemCreateRequestWithDefaults(nodeID, ua.AttributeIDValue, uint32(i))
		if err := assignConfigValuesToRequest(req, &o.NodeMetricMapping[i].Tag.MonitoringParams); err != nil {
			return fmt.Errorf("assigning monitoring params failed: %w", err)
		}
		o.monitoredItemsReqs = append(o.monitoredItemsReqs, req)
	}

	o.Log.Debugf("Creating event streaming items")
	o.eventItemsReqs = make([]*ua.MonitoredItemCreateRequest, 0, len(o.EventNodeMetricMapping))
	for i, node := range o.EventNodeMetricMapping {
		req := opcua.NewMonitoredItemCreateRequestWithDefaults(node.NodeID, ua.AttributeIDEventNotifier, uint32(i))
		if node.SamplingInterval != nil {
			req.RequestedParameters.SamplingInterval = float64(time.Duration(*node.SamplingInterval) / time.Millisecond)
		}
		if node.QueueSize != nil {
			req.RequestedParameters.QueueSize = *node.QueueSize
		}

		filterExtObj, err := node.CreateEventFilter()
		if err != nil {
			return fmt.Errorf("creating event filter failed: %w", err)
		}
		req.RequestedParameters.Filter = filterExtObj
		o.eventItemsReqs = append(o.eventItemsReqs, req)
	}

	o.Log.Debugf("Creating OPC UA subscription")
	o.sub, err = o.Client.Subscribe(o.ctx, &opcua.SubscriptionParameters{
		Interval: time.Duration(o.Config.SubscriptionInterval),
	}, o.dataNotifications)
	if err != nil {
		o.Log.Error("Failed to create subscription")
		return err
	}

	o.Log.Debugf("Subscribed with subscription ID %d", o.sub.SubscriptionID)
	return nil
}

func (o *subscribeClient) stop(ctx context.Context) <-chan struct{} {
	o.Log.Debugf("Stopping OPC subscription...")
	if o.State() != opcuaclient.Connected {
		return nil
	}
	if o.sub != nil {
		if err := o.sub.Cancel(ctx); err != nil {
			o.Log.Warn("Cancelling OPC UA subscription failed with error ", err)
		}
	}
	closing := o.OpcUAInputClient.Stop(ctx)
	o.cancel()
	return closing
}

func (o *subscribeClient) startMonitoring(ctx context.Context) (<-chan telegraf.Metric, error) {
	err := o.connect()
	if err != nil {
		switch o.Config.ConnectFailBehavior {
		case "retry":
			o.Log.Warnf("Failed to connect to OPC UA server %s. Will attempt to connect again at the next interval: %s", o.Config.Endpoint, err)
			return nil, nil
		case "ignore":
			o.Log.Errorf("Failed to connect to OPC UA server %s. Will not retry: %s", o.Config.Endpoint, err)
			return nil, nil
		}
		return nil, err
	}

	var skippedItems int
	if len(o.monitoredItemsReqs) != 0 {
		resp, err := o.sub.Monitor(ctx, ua.TimestampsToReturnBoth, o.monitoredItemsReqs...)
		if err != nil {
			return nil, fmt.Errorf("failed to start monitoring items: %w", err)
		}
		o.Log.Debug("Monitoring items")

		for idx, res := range resp.Results {
			if o.StatusCodeOK(res.StatusCode) {
				continue
			}
			nodeID := "?"
			if len(o.OpcUAInputClient.NodeIDs) > idx {
				nodeID = o.OpcUAInputClient.NodeIDs[idx].String()
			}
			fieldName := o.OpcUAInputClient.NodeMetricMapping[idx].Tag.FieldName
			o.Log.Warnf("Failed to create monitored item for node %v (%v): %v", fieldName, nodeID, res.StatusCode)
			skippedItems++
		}
	}

	if len(o.eventItemsReqs) != 0 {
		resp, err := o.sub.Monitor(ctx, ua.TimestampsToReturnBoth, o.eventItemsReqs...)
		if err != nil {
			return nil, fmt.Errorf("failed to start monitoring event stream: %w", err)
		}
		o.Log.Debug("Monitoring events")

		for idx, res := range resp.Results {
			if o.StatusCodeOK(res.StatusCode) {
				continue
			}
			nodeID := "?"
			if len(o.EventNodeMetricMapping) > idx {
				nodeID = o.EventNodeMetricMapping[idx].NodeID.String()
			}
			o.Log.Warnf("Failed to create monitored event item for node %v: %v", nodeID, res.StatusCode)
			skippedItems++
		}
	}

	totalItems := len(o.monitoredItemsReqs) + len(o.eventItemsReqs)
	if skippedItems > 0 && skippedItems == totalItems {
		o.Log.Warnf("All %d monitored items failed, no data will be collected", totalItems)
	}

	go o.processReceivedNotifications()

	return o.metrics, nil
}

func (o *subscribeClient) processReceivedNotifications() {
	for {
		select {
		case <-o.ctx.Done():
			o.Log.Debug("Processing received notifications stopped")
			return

		case res, ok := <-o.dataNotifications:
			if !ok {
				o.Log.Debugf("Data notification channel closed. Processing of received notifications stopped")
				return
			}
			if res.Error != nil {
				o.Log.Error(res.Error)
				continue
			}
			if res.Value == nil {
				o.Log.Warn("Received nil notification value, skipping")
				continue
			}

			switch notif := res.Value.(type) {
			case *ua.DataChangeNotification:
				o.Log.Debugf("Received data change notification with %d items", len(notif.MonitoredItems))
				// It is assumed the notifications are ordered chronologically
				for _, monitoredItemNotif := range notif.MonitoredItems {
					i := int(monitoredItemNotif.ClientHandle)
					oldValue := o.LastReceivedData[i].Value
					o.UpdateNodeValue(i, monitoredItemNotif.Value)
					o.Log.Debugf("Data change notification: node %q value changed from %v to %v",
						o.NodeIDs[i].String(), oldValue, o.LastReceivedData[i].Value)
					o.metrics <- o.MetricForNode(i)
				}
			case *ua.EventNotificationList:
				o.Log.Debugf("Processing event notification with %d events", len(notif.Events))
				// It is assumed the events are ordered chronologically
				for _, event := range notif.Events {
					i := int(event.ClientHandle)
					if m := o.MetricForEvent(i, event); m != nil {
						o.metrics <- m
					}
				}
			default:
				o.Log.Warnf("Received notification has unexpected type %s", reflect.TypeOf(res.Value))
			}
		}
	}
}
