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

import (
	_ "embed"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"net/url"
	"strings"
	"time"

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

//go:embed sample.conf
var sampleConfig string

var levels = []string{"ok", "warning", "critical", "unknown"}

type Icinga2 struct {
	Server          string          `toml:"server"`
	Objects         []string        `toml:"objects"`
	Status          []string        `toml:"status"`
	Username        string          `toml:"username"`
	Password        string          `toml:"password"`
	ResponseTimeout config.Duration `toml:"response_timeout"`
	tls.ClientConfig

	Log telegraf.Logger `toml:"-"`

	client *http.Client
}

type resultObject struct {
	Results []struct {
		Attrs struct {
			CheckCommand string  `json:"check_command"`
			DisplayName  string  `json:"display_name"`
			Name         string  `json:"name"`
			State        float64 `json:"state"`
			HostName     string  `json:"host_name"`
		} `json:"attrs"`
		Name  string   `json:"name"`
		Joins struct{} `json:"joins"`
		Meta  struct{} `json:"meta"`
		Type  string   `json:"type"`
	} `json:"results"`
}

type resultCIB struct {
	Results []struct {
		Status map[string]interface{} `json:"status"`
	} `json:"results"`
}

type resultPerfdata struct {
	Results []struct {
		Perfdata []struct {
			Label string  `json:"label"`
			Value float64 `json:"value"`
		} `json:"perfdata"`
	} `json:"results"`
}

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

func (i *Icinga2) Init() error {
	statusEndpoints := []string{"ApiListener", "CIB", "IdoMysqlConnection", "IdoPgsqlConnection"}
	if err := choice.CheckSlice(i.Status, statusEndpoints); err != nil {
		return fmt.Errorf("config option 'status': %w", err)
	}

	if i.ResponseTimeout < config.Duration(time.Second) {
		i.ResponseTimeout = config.Duration(time.Second * 5)
	}

	client, err := i.createHTTPClient()
	if err != nil {
		return err
	}
	i.client = client

	objectEndpoints := []string{"services", "hosts"}
	if err := choice.CheckSlice(i.Objects, objectEndpoints); err != nil {
		return fmt.Errorf("config option 'objects': %w", err)
	}

	return nil
}

func (i *Icinga2) Gather(acc telegraf.Accumulator) error {
	// Collect /v1/objects
	for _, objectType := range i.Objects {
		requestURL := "%s/v1/objects/%s?attrs=name&attrs=display_name&attrs=state&attrs=check_command"

		// Note: attrs=host_name is only valid for 'services' requests, using check.Attrs.HostName for the host
		//       'hosts' requests will need to use attrs=name only, using check.Attrs.Name for the host
		if objectType == "services" {
			requestURL += "&attrs=host_name"
		}

		address := fmt.Sprintf(requestURL, i.Server, objectType)

		resp, err := i.icingaRequest(address)
		if err != nil {
			return err
		}

		result := resultObject{}
		err = parseObjectResponse(resp, &result)
		if err != nil {
			return fmt.Errorf("could not parse object response: %w", err)
		}

		i.gatherObjects(acc, result, objectType)
	}

	// Collect /v1/status
	for _, statusType := range i.Status {
		address := fmt.Sprintf("%s/v1/status/%s", i.Server, statusType)

		resp, err := i.icingaRequest(address)
		if err != nil {
			return err
		}

		tags := map[string]string{
			"component": statusType,
		}
		var fields map[string]interface{}

		switch statusType {
		case "ApiListener":
			fields, err = parsePerfdataResponse(resp)
		case "CIB":
			fields, err = parseCIBResponse(resp)
		case "IdoMysqlConnection":
			fields, err = parsePerfdataResponse(resp)
		case "IdoPgsqlConnection":
			fields, err = parsePerfdataResponse(resp)
		}

		if err != nil {
			return fmt.Errorf("could not parse %s response: %w", statusType, err)
		}

		acc.AddFields("icinga2_status", fields, tags)
	}

	return nil
}

func (i *Icinga2) gatherObjects(acc telegraf.Accumulator, checks resultObject, objectType string) {
	for _, check := range checks.Results {
		serverURL, err := url.Parse(i.Server)
		if err != nil {
			i.Log.Error(err.Error())
			continue
		}

		state := int64(check.Attrs.State)

		fields := map[string]interface{}{
			"name":       check.Attrs.Name,
			"state_code": state,
		}

		// source is dependent on 'services' or 'hosts' check
		source := check.Attrs.Name
		if objectType == "services" {
			source = check.Attrs.HostName
		}

		tags := map[string]string{
			"display_name":  check.Attrs.DisplayName,
			"check_command": check.Attrs.CheckCommand,
			"source":        source,
			"state":         levels[state],
			"server":        serverURL.Hostname(),
			"scheme":        serverURL.Scheme,
			"port":          serverURL.Port(),
		}

		acc.AddFields("icinga2_"+objectType, fields, tags)
	}
}

func (i *Icinga2) createHTTPClient() (*http.Client, error) {
	tlsCfg, err := i.ClientConfig.TLSConfig()
	if err != nil {
		return nil, err
	}

	client := &http.Client{
		Transport: &http.Transport{
			TLSClientConfig: tlsCfg,
		},
		Timeout: time.Duration(i.ResponseTimeout),
	}

	return client, nil
}

func (i *Icinga2) icingaRequest(address string) (*http.Response, error) {
	req, err := http.NewRequest("GET", address, nil)
	if err != nil {
		return nil, err
	}

	if i.Username != "" {
		req.SetBasicAuth(i.Username, i.Password)
	}

	resp, err := i.client.Do(req)
	if err != nil {
		return nil, err
	}

	return resp, nil
}

func parseObjectResponse(resp *http.Response, result *resultObject) error {
	err := json.NewDecoder(resp.Body).Decode(&result)
	if err != nil {
		return err
	}
	err = resp.Body.Close()
	if err != nil {
		return err
	}

	return nil
}

func parseCIBResponse(resp *http.Response) (map[string]interface{}, error) {
	result := resultCIB{}

	err := json.NewDecoder(resp.Body).Decode(&result)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	if len(result.Results) == 0 {
		return nil, errors.New("no results in Icinga2 API response")
	}

	return result.Results[0].Status, nil
}

func parsePerfdataResponse(resp *http.Response) (map[string]interface{}, error) {
	result := resultPerfdata{}

	err := json.NewDecoder(resp.Body).Decode(&result)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	if len(result.Results) == 0 {
		return nil, errors.New("no results in Icinga2 API response")
	}

	fields := make(map[string]interface{})
	for _, item := range result.Results[0].Perfdata {
		i := strings.Index(item.Label, "-")
		if i > 0 {
			fields[item.Label[i+1:]] = item.Value
		} else {
			fields[item.Label] = item.Value
		}
	}

	return fields, nil
}

func init() {
	inputs.Add("icinga2", func() telegraf.Input {
		return &Icinga2{
			Server:          "https://localhost:5665",
			Objects:         []string{"services"},
			ResponseTimeout: config.Duration(time.Second * 5),
		}
	})
}
