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

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

	"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

type Redfish struct {
	Address          string          `toml:"address"`
	Username         config.Secret   `toml:"username"`
	Password         config.Secret   `toml:"password"`
	ComputerSystemID string          `toml:"computer_system_id"`
	IncludeMetrics   []string        `toml:"include_metrics"`
	IncludeTagSets   []string        `toml:"include_tag_sets"`
	Workarounds      []string        `toml:"workarounds"`
	Timeout          config.Duration `toml:"timeout"`

	tagSet map[string]bool
	client http.Client
	tls.ClientConfig
	baseURL *url.URL
}

const (
	// tag sets used for including redfish OData link parent data
	tagSetChassisLocation = "chassis.location"
	tagSetChassis         = "chassis"
)

type system struct {
	Hostname string `json:"hostname"`
	Links    struct {
		Chassis []struct {
			Ref string `json:"@odata.id"`
		}
	}
}

type chassis struct {
	ChassisType  string
	Location     *location
	Manufacturer string
	Model        string
	PartNumber   string
	Power        struct {
		Ref string `json:"@odata.id"`
	}
	PowerState   string
	SKU          string
	SerialNumber string
	Status       status
	Thermal      struct {
		Ref string `json:"@odata.id"`
	}
}

type power struct {
	PowerControl []struct {
		Name                string
		MemberID            string
		PowerAllocatedWatts *float64
		PowerAvailableWatts *float64
		PowerCapacityWatts  *float64
		PowerConsumedWatts  *float64
		PowerRequestedWatts *float64
		PowerMetrics        struct {
			AverageConsumedWatts *float64
			IntervalInMin        int
			MaxConsumedWatts     *float64
			MinConsumedWatts     *float64
		}
	}
	PowerSupplies []struct {
		Name                 string
		MemberID             string
		PowerInputWatts      *float64
		PowerCapacityWatts   *float64
		PowerOutputWatts     *float64
		LastPowerOutputWatts *float64
		Status               status
		LineInputVoltage     *float64
	}
	Voltages []struct {
		Name                   string
		MemberID               string
		ReadingVolts           *float64
		UpperThresholdCritical *float64
		UpperThresholdFatal    *float64
		LowerThresholdCritical *float64
		LowerThresholdFatal    *float64
		Status                 status
	}
}

type thermal struct {
	Fans []struct {
		Name                   string
		MemberID               string
		FanName                string
		CurrentReading         *int64
		Reading                *int64
		ReadingUnits           *string
		UpperThresholdCritical *int64
		UpperThresholdFatal    *int64
		LowerThresholdCritical *int64
		LowerThresholdFatal    *int64
		Status                 status
	}
	Temperatures []struct {
		Name                   string
		MemberID               string
		ReadingCelsius         *float64
		UpperThresholdCritical *float64
		UpperThresholdFatal    *float64
		LowerThresholdCritical *float64
		LowerThresholdFatal    *float64
		Status                 status
	}
}

type location struct {
	PostalAddress struct {
		DataCenter string
		Room       string
	}
	Placement struct {
		Rack string
		Row  string
	}
}

type status struct {
	State  string
	Health string
}

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

func (r *Redfish) Init() error {
	if r.Address == "" {
		return errors.New("did not provide IP")
	}

	if r.Username.Empty() && r.Password.Empty() {
		return errors.New("did not provide username and password")
	}

	if r.ComputerSystemID == "" {
		return errors.New("did not provide the computer system ID of the resource")
	}

	if len(r.IncludeMetrics) == 0 {
		return errors.New("no metrics specified to collect")
	}
	for _, metric := range r.IncludeMetrics {
		switch metric {
		case "thermal", "power":
		default:
			return fmt.Errorf("unknown metric requested: %s", metric)
		}
	}

	for _, workaround := range r.Workarounds {
		switch workaround {
		case "ilo4-thermal":
		default:
			return fmt.Errorf("unknown workaround requested: %s", workaround)
		}
	}
	r.tagSet = make(map[string]bool, len(r.IncludeTagSets))
	for _, setLabel := range r.IncludeTagSets {
		r.tagSet[setLabel] = true
	}

	var err error
	r.baseURL, err = url.Parse(r.Address)
	if err != nil {
		return err
	}

	tlsCfg, err := r.ClientConfig.TLSConfig()
	if err != nil {
		return err
	}

	r.client = http.Client{
		Transport: &http.Transport{
			TLSClientConfig: tlsCfg,
			Proxy:           http.ProxyFromEnvironment,
		},
		Timeout: time.Duration(r.Timeout),
	}

	return nil
}

func (r *Redfish) getData(address string, payload interface{}) error {
	req, err := http.NewRequest("GET", address, nil)
	if err != nil {
		return err
	}

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

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

	req.SetBasicAuth(user, pass)
	req.Header.Set("Accept", "application/json")
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("OData-Version", "4.0")

	// workaround for iLO4 thermal data
	if slices.Contains(r.Workarounds, "ilo4-thermal") && strings.Contains(address, "/Thermal") {
		req.Header.Del("OData-Version")
	}

	resp, err := r.client.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	if resp.StatusCode != 200 {
		return fmt.Errorf("received status code %d (%s) for address %s, expected 200",
			resp.StatusCode,
			http.StatusText(resp.StatusCode),
			r.Address)
	}

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return err
	}

	err = json.Unmarshal(body, &payload)
	if err != nil {
		return fmt.Errorf("error parsing input: %w", err)
	}

	return nil
}

func (r *Redfish) getComputerSystem(id string) (*system, error) {
	loc := r.baseURL.ResolveReference(&url.URL{Path: path.Join("/redfish/v1/Systems/", id)})
	system := &system{}
	err := r.getData(loc.String(), system)
	if err != nil {
		return nil, err
	}
	return system, nil
}

func (r *Redfish) getChassis(ref string) (*chassis, error) {
	loc := r.baseURL.ResolveReference(&url.URL{Path: ref})
	chassis := &chassis{}
	err := r.getData(loc.String(), chassis)
	if err != nil {
		return nil, err
	}
	return chassis, nil
}

func (r *Redfish) getPower(ref string) (*power, error) {
	loc := r.baseURL.ResolveReference(&url.URL{Path: ref})
	power := &power{}
	err := r.getData(loc.String(), power)
	if err != nil {
		return nil, err
	}
	return power, nil
}

func (r *Redfish) getThermal(ref string) (*thermal, error) {
	loc := r.baseURL.ResolveReference(&url.URL{Path: ref})
	thermal := &thermal{}
	err := r.getData(loc.String(), thermal)
	if err != nil {
		return nil, err
	}
	return thermal, nil
}

func setChassisTags(chassis *chassis, tags map[string]string) {
	tags["chassis_chassistype"] = chassis.ChassisType
	tags["chassis_manufacturer"] = chassis.Manufacturer
	tags["chassis_model"] = chassis.Model
	tags["chassis_partnumber"] = chassis.PartNumber
	tags["chassis_powerstate"] = chassis.PowerState
	tags["chassis_sku"] = chassis.SKU
	tags["chassis_serialnumber"] = chassis.SerialNumber
	tags["chassis_state"] = chassis.Status.State
	tags["chassis_health"] = chassis.Status.Health
}

func (r *Redfish) Gather(acc telegraf.Accumulator) error {
	address, _, err := net.SplitHostPort(r.baseURL.Host)
	if err != nil {
		address = r.baseURL.Host
	}

	system, err := r.getComputerSystem(r.ComputerSystemID)
	if err != nil {
		return err
	}

	for _, link := range system.Links.Chassis {
		chassis, err := r.getChassis(link.Ref)
		if err != nil {
			return err
		}

		for _, metric := range r.IncludeMetrics {
			var err error
			switch metric {
			case "thermal":
				err = r.gatherThermal(acc, address, system, chassis)
			case "power":
				err = r.gatherPower(acc, address, system, chassis)
			default:
				return fmt.Errorf("unknown metric requested: %s", metric)
			}
			if err != nil {
				return err
			}
		}
	}
	return nil
}

func (r *Redfish) gatherThermal(acc telegraf.Accumulator, address string, system *system, chassis *chassis) error {
	thermal, err := r.getThermal(chassis.Thermal.Ref)
	if err != nil {
		return err
	}

	for _, j := range thermal.Temperatures {
		tags := make(map[string]string, 19)
		tags["member_id"] = j.MemberID
		tags["address"] = address
		tags["name"] = j.Name
		tags["source"] = system.Hostname
		tags["state"] = j.Status.State
		tags["health"] = j.Status.Health
		if _, ok := r.tagSet[tagSetChassisLocation]; ok && chassis.Location != nil {
			tags["datacenter"] = chassis.Location.PostalAddress.DataCenter
			tags["room"] = chassis.Location.PostalAddress.Room
			tags["rack"] = chassis.Location.Placement.Rack
			tags["row"] = chassis.Location.Placement.Row
		}
		if _, ok := r.tagSet[tagSetChassis]; ok {
			setChassisTags(chassis, tags)
		}

		fields := make(map[string]interface{})
		fields["reading_celsius"] = j.ReadingCelsius
		fields["upper_threshold_critical"] = j.UpperThresholdCritical
		fields["upper_threshold_fatal"] = j.UpperThresholdFatal
		fields["lower_threshold_critical"] = j.LowerThresholdCritical
		fields["lower_threshold_fatal"] = j.LowerThresholdFatal
		acc.AddFields("redfish_thermal_temperatures", fields, tags)
	}

	for _, j := range thermal.Fans {
		tags := make(map[string]string, 20)
		fields := make(map[string]interface{}, 5)
		tags["member_id"] = j.MemberID
		tags["address"] = address
		tags["name"] = j.Name
		if j.FanName != "" {
			tags["name"] = j.FanName
		}
		tags["source"] = system.Hostname
		tags["state"] = j.Status.State
		tags["health"] = j.Status.Health
		if _, ok := r.tagSet[tagSetChassisLocation]; ok && chassis.Location != nil {
			tags["datacenter"] = chassis.Location.PostalAddress.DataCenter
			tags["room"] = chassis.Location.PostalAddress.Room
			tags["rack"] = chassis.Location.Placement.Rack
			tags["row"] = chassis.Location.Placement.Row
		}
		if _, ok := r.tagSet[tagSetChassis]; ok {
			setChassisTags(chassis, tags)
		}

		if j.ReadingUnits != nil && *j.ReadingUnits == "RPM" {
			fields["upper_threshold_critical"] = j.UpperThresholdCritical
			fields["upper_threshold_fatal"] = j.UpperThresholdFatal
			fields["lower_threshold_critical"] = j.LowerThresholdCritical
			fields["lower_threshold_fatal"] = j.LowerThresholdFatal
			fields["reading_rpm"] = j.Reading
		} else if j.CurrentReading != nil {
			fields["reading_percent"] = j.CurrentReading
		} else {
			fields["reading_percent"] = j.Reading
		}
		acc.AddFields("redfish_thermal_fans", fields, tags)
	}

	return nil
}

func (r *Redfish) gatherPower(acc telegraf.Accumulator, address string, system *system, chassis *chassis) error {
	power, err := r.getPower(chassis.Power.Ref)
	if err != nil {
		return err
	}

	for _, j := range power.PowerControl {
		tags := map[string]string{
			"member_id": j.MemberID,
			"address":   address,
			"name":      j.Name,
			"source":    system.Hostname,
		}
		if _, ok := r.tagSet[tagSetChassisLocation]; ok && chassis.Location != nil {
			tags["datacenter"] = chassis.Location.PostalAddress.DataCenter
			tags["room"] = chassis.Location.PostalAddress.Room
			tags["rack"] = chassis.Location.Placement.Rack
			tags["row"] = chassis.Location.Placement.Row
		}
		if _, ok := r.tagSet[tagSetChassis]; ok {
			setChassisTags(chassis, tags)
		}

		fields := map[string]interface{}{
			"power_allocated_watts":  j.PowerAllocatedWatts,
			"power_available_watts":  j.PowerAvailableWatts,
			"power_capacity_watts":   j.PowerCapacityWatts,
			"power_consumed_watts":   j.PowerConsumedWatts,
			"power_requested_watts":  j.PowerRequestedWatts,
			"average_consumed_watts": j.PowerMetrics.AverageConsumedWatts,
			"interval_in_min":        j.PowerMetrics.IntervalInMin,
			"max_consumed_watts":     j.PowerMetrics.MaxConsumedWatts,
			"min_consumed_watts":     j.PowerMetrics.MinConsumedWatts,
		}

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

	for _, j := range power.PowerSupplies {
		tags := make(map[string]string, 19)
		tags["member_id"] = j.MemberID
		tags["address"] = address
		tags["name"] = j.Name
		tags["source"] = system.Hostname
		tags["state"] = j.Status.State
		tags["health"] = j.Status.Health
		if _, ok := r.tagSet[tagSetChassisLocation]; ok && chassis.Location != nil {
			tags["datacenter"] = chassis.Location.PostalAddress.DataCenter
			tags["room"] = chassis.Location.PostalAddress.Room
			tags["rack"] = chassis.Location.Placement.Rack
			tags["row"] = chassis.Location.Placement.Row
		}
		if _, ok := r.tagSet[tagSetChassis]; ok {
			setChassisTags(chassis, tags)
		}

		fields := make(map[string]interface{})
		fields["power_input_watts"] = j.PowerInputWatts
		fields["power_output_watts"] = j.PowerOutputWatts
		fields["line_input_voltage"] = j.LineInputVoltage
		fields["last_power_output_watts"] = j.LastPowerOutputWatts
		fields["power_capacity_watts"] = j.PowerCapacityWatts
		acc.AddFields("redfish_power_powersupplies", fields, tags)
	}

	for _, j := range power.Voltages {
		tags := make(map[string]string, 19)
		tags["member_id"] = j.MemberID
		tags["address"] = address
		tags["name"] = j.Name
		tags["source"] = system.Hostname
		tags["state"] = j.Status.State
		tags["health"] = j.Status.Health
		if _, ok := r.tagSet[tagSetChassisLocation]; ok && chassis.Location != nil {
			tags["datacenter"] = chassis.Location.PostalAddress.DataCenter
			tags["room"] = chassis.Location.PostalAddress.Room
			tags["rack"] = chassis.Location.Placement.Rack
			tags["row"] = chassis.Location.Placement.Row
		}
		if _, ok := r.tagSet[tagSetChassis]; ok {
			setChassisTags(chassis, tags)
		}

		fields := make(map[string]interface{})
		fields["reading_volts"] = j.ReadingVolts
		fields["upper_threshold_critical"] = j.UpperThresholdCritical
		fields["upper_threshold_fatal"] = j.UpperThresholdFatal
		fields["lower_threshold_critical"] = j.LowerThresholdCritical
		fields["lower_threshold_fatal"] = j.LowerThresholdFatal
		acc.AddFields("redfish_power_voltages", fields, tags)
	}

	return nil
}

func init() {
	inputs.Add("redfish", func() telegraf.Input {
		return &Redfish{
			// default tag set of chassis.location required for backwards compatibility
			IncludeTagSets: []string{tagSetChassisLocation},
			IncludeMetrics: []string{"power", "thermal"},
		}
	})
}
