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

import (
	"context"
	_ "embed"
	"errors"
	"sync"
	"time"

	"github.com/vmware/govmomi/vim25/soap"

	"github.com/influxdata/telegraf"
	"github.com/influxdata/telegraf/config"
	"github.com/influxdata/telegraf/plugins/common/tls"
	"github.com/influxdata/telegraf/plugins/inputs"
)

//go:embed sample.conf
var sampleConfig string

// VSphere is the top level type for the vSphere input plugin. It contains all the configuration
// and a list of connected vSphere endpoints
type VSphere struct {
	Vcenters                    []string
	Username                    config.Secret `toml:"username"`
	Password                    config.Secret `toml:"password"`
	DatacenterInstances         bool
	DatacenterMetricInclude     []string
	DatacenterMetricExclude     []string
	DatacenterInclude           []string
	DatacenterExclude           []string
	ClusterInstances            bool
	ClusterMetricInclude        []string
	ClusterMetricExclude        []string
	ClusterInclude              []string
	ClusterExclude              []string
	ResourcePoolInstances       bool
	ResourcePoolMetricInclude   []string
	ResourcePoolMetricExclude   []string
	ResourcePoolInclude         []string
	ResourcePoolExclude         []string
	HostInstances               bool
	HostMetricInclude           []string
	HostMetricExclude           []string
	HostInclude                 []string
	HostExclude                 []string
	VMInstances                 bool     `toml:"vm_instances"`
	VMMetricInclude             []string `toml:"vm_metric_include"`
	VMMetricExclude             []string `toml:"vm_metric_exclude"`
	VMInclude                   []string `toml:"vm_include"`
	VMExclude                   []string `toml:"vm_exclude"`
	DatastoreInstances          bool
	DatastoreMetricInclude      []string
	DatastoreMetricExclude      []string
	DatastoreInclude            []string
	DatastoreExclude            []string
	Separator                   string
	CustomAttributeInclude      []string
	CustomAttributeExclude      []string
	UseIntSamples               bool
	IPAddresses                 []string
	MetricLookback              int
	DisconnectedServersBehavior string

	MaxQueryObjects         int
	MaxQueryMetrics         int
	CollectConcurrency      int
	DiscoverConcurrency     int
	ForceDiscoverOnInit     bool `toml:"force_discover_on_init" deprecated:"1.14.0;option is ignored"`
	ObjectDiscoveryInterval config.Duration
	Timeout                 config.Duration
	HistoricalInterval      config.Duration

	endpoints []*Endpoint
	cancel    context.CancelFunc

	// Mix in the TLS/SSL goodness from core
	tls.ClientConfig

	Log telegraf.Logger
}

func (*VSphere) SampleConfig() string {
	return sampleConfig
}

// Start is called from telegraf core when a plugin is started and allows it to
// perform initialization tasks.
func (v *VSphere) Start(_ telegraf.Accumulator) error {
	v.Log.Info("Starting plugin")
	ctx, cancel := context.WithCancel(context.Background())
	v.cancel = cancel

	// Create endpoints, one for each vCenter we're monitoring
	v.endpoints = make([]*Endpoint, 0, len(v.Vcenters))
	for _, rawURL := range v.Vcenters {
		u, err := soap.ParseURL(rawURL)
		if err != nil {
			return err
		}
		ep, err := NewEndpoint(ctx, v, u, v.Log)
		if err != nil {
			return err
		}
		v.endpoints = append(v.endpoints, ep)
	}
	return nil
}

// Stop is called from telegraf core when a plugin is stopped and allows it to
// perform shutdown tasks.
func (v *VSphere) Stop() {
	v.Log.Info("Stopping plugin")
	v.cancel()

	// Wait for all endpoints to finish. No need to wait for
	// Gather() to finish here, since it Stop() will only be called
	// after the last Gather() has finished. We do, however, need to
	// wait for any discovery to complete by trying to grab the
	// "busy" mutex.
	for _, ep := range v.endpoints {
		v.Log.Debugf("Waiting for endpoint %q to finish", ep.URL.Host)
		func() {
			ep.busy.Lock() // Wait until discovery is finished
			defer ep.busy.Unlock()
			ep.Close()
		}()
	}
}

// Gather is the main data collection function called by the Telegraf core. It performs all
// the data collection and writes all metrics into the Accumulator passed as an argument.
func (v *VSphere) Gather(acc telegraf.Accumulator) error {
	var wg sync.WaitGroup
	for _, ep := range v.endpoints {
		wg.Add(1)
		go func(endpoint *Endpoint) {
			defer wg.Done()
			err := endpoint.Collect(context.Background(), acc)
			if errors.Is(err, context.Canceled) {
				// No need to signal errors if we were merely canceled.
				err = nil
			}
			if err != nil {
				acc.AddError(err)
			}
		}(ep)
	}

	wg.Wait()
	return nil
}

func init() {
	inputs.Add("vsphere", func() telegraf.Input {
		return &VSphere{
			Vcenters: []string{},

			DatacenterInstances:       false,
			DatacenterMetricInclude:   nil,
			DatacenterMetricExclude:   nil,
			DatacenterInclude:         []string{"/*"},
			ClusterInstances:          false,
			ClusterMetricInclude:      nil,
			ClusterMetricExclude:      nil,
			ClusterInclude:            []string{"/*/host/**"},
			HostInstances:             true,
			HostMetricInclude:         nil,
			HostMetricExclude:         nil,
			HostInclude:               []string{"/*/host/**"},
			ResourcePoolInstances:     false,
			ResourcePoolMetricInclude: nil,
			ResourcePoolMetricExclude: nil,
			ResourcePoolInclude:       []string{"/*/host/**"},
			VMInstances:               true,
			VMMetricInclude:           nil,
			VMMetricExclude:           nil,
			VMInclude:                 []string{"/*/vm/**"},
			DatastoreInstances:        false,
			DatastoreMetricInclude:    nil,
			DatastoreMetricExclude:    nil,
			DatastoreInclude:          []string{"/*/datastore/**"},
			Separator:                 "_",
			CustomAttributeInclude:    []string{},
			CustomAttributeExclude:    []string{"*"},
			UseIntSamples:             true,
			IPAddresses:               []string{},

			MaxQueryObjects:             256,
			MaxQueryMetrics:             256,
			CollectConcurrency:          1,
			DiscoverConcurrency:         1,
			MetricLookback:              3,
			ForceDiscoverOnInit:         true,
			ObjectDiscoveryInterval:     config.Duration(time.Second * 300),
			Timeout:                     config.Duration(time.Second * 60),
			HistoricalInterval:          config.Duration(time.Second * 300),
			DisconnectedServersBehavior: "error",
		}
	})
}
