//go:generate ../../../tools/readme_config_includer/generator
package ctrlx_datalayer

import (
	"bytes"
	"context"
	_ "embed"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"net/url"
	"strconv"
	"sync"
	"time"

	"github.com/boschrexroth/ctrlx-datalayer-golang/pkg/sseclient"
	"github.com/boschrexroth/ctrlx-datalayer-golang/pkg/token"
	"github.com/google/uuid"

	"github.com/influxdata/telegraf"
	"github.com/influxdata/telegraf/config"
	"github.com/influxdata/telegraf/internal/choice"
	"github.com/influxdata/telegraf/metric"
	httpconfig "github.com/influxdata/telegraf/plugins/common/http"
	"github.com/influxdata/telegraf/plugins/inputs"
	jsonParser "github.com/influxdata/telegraf/plugins/parsers/json"
)

// This plugin is based on the official ctrlX CORE API. Documentation can be found in OpenAPI format at:
// https://boschrexroth.github.io/rest-api-description/ctrlx-automation/ctrlx-core/
// Used APIs are:
// * ctrlX CORE - Authorization and Authentication API
// * ctrlX CORE - Data Layer API
//
// All communication between the device and this input plugin is based
// on https REST and HTML5 Server Sent Events (sse).

//go:embed sample.conf
var sampleConfig string

// CtrlXDataLayer encapsulated the configuration as well as the state of this plugin.
type CtrlXDataLayer struct {
	Server   string        `toml:"server"`
	Username config.Secret `toml:"username"`
	Password config.Secret `toml:"password"`

	Log          telegraf.Logger `toml:"-"`
	Subscription []Subscription

	url    string
	wg     sync.WaitGroup
	cancel context.CancelFunc

	acc          telegraf.Accumulator
	connection   *http.Client
	tokenManager token.TokenManager
	httpconfig.HTTPClientConfig
}

// convertTimestamp2UnixTime converts the given Data Layer timestamp of the payload to UnixTime.
func convertTimestamp2UnixTime(t int64) time.Time {
	// 1 sec=1000 millisec=1000000 microsec=1000000000 nanosec.
	// Convert from FILETIME (100-nanosecond intervals since January 1, 1601 UTC) to
	// seconds and nanoseconds since January 1, 1970 UTC.
	// Between Jan 1, 1601 and Jan 1, 1970 there are 11644473600 seconds.
	return time.Unix(0, (t-116444736000000000)*100)
}

// createSubscription uses the official 'ctrlX Data Layer API' to create the sse subscription.
func (c *CtrlXDataLayer) createSubscription(sub *Subscription) (string, error) {
	sseURL := c.url + subscriptionPath

	id := "telegraf_" + uuid.New().String()
	request := sub.createRequest(id)
	payload, err := json.Marshal(request)
	if err != nil {
		return "", fmt.Errorf("failed to create subscription %d payload: %w", sub.index, err)
	}

	requestBody := bytes.NewBuffer(payload)
	req, err := http.NewRequest("POST", sseURL, requestBody)
	if err != nil {
		return "", fmt.Errorf("failed to create subscription %d request: %w", sub.index, err)
	}

	req.Header.Add("Authorization", c.tokenManager.Token.String())

	resp, err := c.connection.Do(req)
	if err != nil {
		return "", fmt.Errorf("failed to do request to create sse subscription %d: %w", sub.index, err)
	}
	resp.Body.Close()

	if resp.StatusCode != 200 && resp.StatusCode != 201 {
		return "", fmt.Errorf("failed to create sse subscription %d, status: %s", sub.index, resp.Status)
	}

	return sseURL + "/" + id, nil
}

// createSubscriptionAndSseClient creates a sse subscription on the server and
// initializes a sse client to receive sse events from the server.
func (c *CtrlXDataLayer) createSubscriptionAndSseClient(sub *Subscription) (*sseclient.SseClient, error) {
	t, err := c.tokenManager.RequestAuthToken()
	if err != nil {
		return nil, err
	}

	subURL, err := c.createSubscription(sub)
	if err != nil {
		return nil, err
	}

	client := sseclient.NewSseClient(subURL, t.String(), c.InsecureSkipVerify)

	return client, nil
}

// addMetric writes sse metric into accumulator.
func (c *CtrlXDataLayer) addMetric(se *sseclient.SseEvent, sub *Subscription) {
	switch se.Event {
	case "update":
		// Received an updated value, that we translate into a metric
		var d sseEventData

		if err := json.Unmarshal([]byte(se.Data), &d); err != nil {
			c.acc.AddError(fmt.Errorf("received malformed data from 'update' event: %w", err))
			return
		}
		m, err := c.createMetric(&d, sub)
		if err != nil {
			c.acc.AddError(fmt.Errorf("failed to create metrics: %w", err))
			return
		}
		c.acc.AddMetric(m)
	case "error":
		// Received an error event, that we report to the accumulator
		var e sseEventError
		if err := json.Unmarshal([]byte(se.Data), &e); err != nil {
			c.acc.AddError(fmt.Errorf("received malformed data from 'error' event: %w", err))
			return
		}
		c.acc.AddError(fmt.Errorf("received 'error' event for node: %q", e.Instance))
	case "keepalive":
		// Keepalive events are ignored for the moment
		c.Log.Debug("Received keepalive event")
	default:
		// Received a yet unsupported event type
		c.Log.Debugf("Received unsupported event: %q", se.Event)
	}
}

// createMetric - create metric depending on flag 'output_json' and data type
func (c *CtrlXDataLayer) createMetric(em *sseEventData, sub *Subscription) (telegraf.Metric, error) {
	t := convertTimestamp2UnixTime(em.Timestamp)
	node := sub.node(em.Node)
	if node == nil {
		return nil, errors.New("node not found")
	}

	// default tags
	tags := map[string]string{
		"node":   em.Node,
		"source": c.Server,
	}

	// add tags of subscription if user has defined
	for key, value := range sub.Tags {
		tags[key] = value
	}

	// add tags of node if user has defined
	for key, value := range node.Tags {
		tags[key] = value
	}

	// set measurement of subscription
	measurement := sub.Measurement

	// get field key from node properties
	fieldKey := node.fieldKey()

	if fieldKey == "" {
		return nil, errors.New("field key not valid")
	}

	if sub.OutputJSONString {
		b, err := json.Marshal(em.Value)
		if err != nil {
			return nil, err
		}
		fields := map[string]interface{}{fieldKey: string(b)}
		m := metric.New(measurement, tags, fields, t)
		return m, nil
	}

	switch em.Type {
	case "object":
		flattener := jsonParser.JSONFlattener{}
		err := flattener.FullFlattenJSON(fieldKey, em.Value, true, true)
		if err != nil {
			return nil, err
		}

		m := metric.New(measurement, tags, flattener.Fields, t)
		return m, nil
	case "arbool8",
		"arint8", "aruint8",
		"arint16", "aruint16",
		"arint32", "aruint32",
		"arint64", "aruint64",
		"arfloat", "ardouble",
		"arstring",
		"artimestamp":
		fields := make(map[string]interface{})
		values := em.Value.([]interface{})
		for i := 0; i < len(values); i++ {
			index := strconv.Itoa(i)
			key := fieldKey + "_" + index
			fields[key] = values[i]
		}
		m := metric.New(measurement, tags, fields, t)
		return m, nil
	case "bool8",
		"int8", "uint8",
		"int16", "uint16",
		"int32", "uint32",
		"int64", "uint64",
		"float", "double",
		"string",
		"timestamp":
		fields := map[string]interface{}{fieldKey: em.Value}
		m := metric.New(measurement, tags, fields, t)
		return m, nil
	}

	return nil, fmt.Errorf("unsupported value type: %s", em.Type)
}

// Init is for setup, and validating config
func (c *CtrlXDataLayer) Init() error {
	// Check all configured subscriptions for valid settings
	for i := range c.Subscription {
		sub := &c.Subscription[i]
		sub.applyDefaultSettings()
		if !choice.Contains(sub.QueueBehaviour, queueBehaviours) {
			c.Log.Infof("The right queue behaviour values are %v", queueBehaviours)
			return fmt.Errorf("subscription %d: setting 'queue_behaviour' %q is invalid", i, sub.QueueBehaviour)
		}
		if !choice.Contains(sub.ValueChange, valueChanges) {
			c.Log.Infof("The right value change values are %v", valueChanges)
			return fmt.Errorf("subscription %d: setting 'value_change' %q is invalid", i, sub.ValueChange)
		}
		if len(sub.Nodes) == 0 {
			c.Log.Warn("A configured subscription has no nodes configured")
		}
		sub.index = i
	}

	// Generate valid communication url based on configured server address
	u := url.URL{
		Scheme: "https",
		Host:   c.Server,
	}
	c.url = u.String()
	if _, err := url.Parse(c.url); err != nil {
		return errors.New("invalid server address")
	}

	return nil
}

// Start input as service, retain the accumulator, establish the connection.
func (c *CtrlXDataLayer) Start(acc telegraf.Accumulator) error {
	var ctx context.Context
	ctx, c.cancel = context.WithCancel(context.Background())

	var err error
	c.connection, err = c.HTTPClientConfig.CreateClient(ctx, c.Log)
	if err != nil {
		return fmt.Errorf("failed to create http client: %w", err)
	}

	username, err := c.Username.Get()
	if err != nil {
		return fmt.Errorf("getting username failed: %w", err)
	}

	password, err := c.Password.Get()
	if err != nil {
		username.Destroy()
		return fmt.Errorf("getting password failed: %w", err)
	}

	c.tokenManager = token.TokenManager{
		Url:        c.url,
		Username:   username.String(),
		Password:   password.String(),
		Connection: c.connection,
	}
	username.Destroy()
	password.Destroy()

	c.acc = acc

	c.gatherLoop(ctx)

	return nil
}

// gatherLoop creates sse subscriptions on the Data Layer and requests the sse data
// the connection will be restablished if the sse subscription is broken.
func (c *CtrlXDataLayer) gatherLoop(ctx context.Context) {
	for _, sub := range c.Subscription {
		c.wg.Add(1)
		go func(sub Subscription) {
			defer c.wg.Done()
			for {
				select {
				case <-ctx.Done():
					c.Log.Debugf("Gather loop for subscription %d stopped", sub.index)
					return
				default:
					client, err := c.createSubscriptionAndSseClient(&sub)
					if err != nil {
						c.Log.Errorf("Creating sse client to subscription %d: %v", sub.index, err)
						time.Sleep(time.Duration(defaultReconnectInterval))
						continue
					}
					c.Log.Debugf("Created sse client to subscription %d", sub.index)

					// Establish connection and handle events in a callback function.
					err = client.Subscribe(ctx, func(event string, data string) {
						c.addMetric(&sseclient.SseEvent{
							Event: event,
							Data:  data,
						}, &sub)
					})
					if errors.Is(err, context.Canceled) {
						// Subscription cancelled
						c.Log.Debugf("Requesting data of subscription %d cancelled", sub.index)
						return
					}
					c.Log.Errorf("Requesting data of subscription %d failed: %v", sub.index, err)
				}
			}
		}(sub)
	}
}

// Stop input as service.
func (c *CtrlXDataLayer) Stop() {
	c.cancel()
	c.wg.Wait()
}

// Gather is called by telegraf to collect the metrics.
func (c *CtrlXDataLayer) Gather(_ telegraf.Accumulator) error {
	// Metrics are sent to the accumulator asynchronously in worker thread. So nothing to do here.
	return nil
}

// SampleConfig returns the auto-inserted sample configuration to the telegraf.
func (*CtrlXDataLayer) SampleConfig() string {
	return sampleConfig
}

// init registers the plugin in telegraf.
func init() {
	inputs.Add("ctrlx_datalayer", func() telegraf.Input {
		return &CtrlXDataLayer{}
	})
}
