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

import (
	_ "embed"
	"encoding/json"
	"fmt"
	"os/exec"
	"strconv"
	"strings"
	"time"

	"github.com/influxdata/telegraf"
	"github.com/influxdata/telegraf/internal"
	"github.com/influxdata/telegraf/plugins/inputs"
)

//go:embed sample.conf
var sampleConfig string

var (
	execCommand = exec.Command
)

type LVM struct {
	UseSudo   bool   `toml:"use_sudo"`
	PVSBinary string `toml:"pvs_binary"`
	VGSBinary string `toml:"vgs_binary"`
	LVSBinary string `toml:"lvs_binary"`
}

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

func (lvm *LVM) Init() error {
	return nil
}

func (lvm *LVM) Gather(acc telegraf.Accumulator) error {
	if err := lvm.gatherPhysicalVolumes(acc); err != nil {
		return err
	} else if err := lvm.gatherVolumeGroups(acc); err != nil {
		return err
	} else if err := lvm.gatherLogicalVolumes(acc); err != nil {
		return err
	}

	return nil
}

func (lvm *LVM) gatherPhysicalVolumes(acc telegraf.Accumulator) error {
	args := []string{
		"--reportformat", "json", "--units", "b", "--nosuffix",
		"-o", "pv_name,vg_name,pv_size,pv_free,pv_used",
	}
	out, err := lvm.runCmd(lvm.PVSBinary, args)
	if err != nil {
		return err
	}

	var report pvsReport
	err = json.Unmarshal(out, &report)
	if err != nil {
		return fmt.Errorf("failed to unmarshal physical volume JSON: %w", err)
	}

	if len(report.Report) > 0 {
		for _, pv := range report.Report[0].Pv {
			tags := map[string]string{
				"path":      pv.Name,
				"vol_group": pv.VolGroup,
			}

			size, err := strconv.ParseUint(pv.Size, 10, 64)
			if err != nil {
				return err
			}

			free, err := strconv.ParseUint(pv.Free, 10, 64)
			if err != nil {
				return err
			}

			used, err := strconv.ParseUint(pv.Used, 10, 64)
			if err != nil {
				return err
			}

			usedPercent := float64(used) / float64(size) * 100

			fields := map[string]interface{}{
				"size":         size,
				"free":         free,
				"used":         used,
				"used_percent": usedPercent,
			}

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

	return nil
}

func (lvm *LVM) gatherVolumeGroups(acc telegraf.Accumulator) error {
	args := []string{
		"--reportformat", "json", "--units", "b", "--nosuffix",
		"-o", "vg_name,pv_count,lv_count,snap_count,vg_size,vg_free",
	}
	out, err := lvm.runCmd(lvm.VGSBinary, args)
	if err != nil {
		return err
	}

	var report vgsReport
	err = json.Unmarshal(out, &report)
	if err != nil {
		return fmt.Errorf("failed to unmarshal vol group JSON: %w", err)
	}

	if len(report.Report) > 0 {
		for _, vg := range report.Report[0].Vg {
			tags := map[string]string{
				"name": vg.Name,
			}

			size, err := strconv.ParseUint(vg.Size, 10, 64)
			if err != nil {
				return err
			}

			free, err := strconv.ParseUint(vg.Free, 10, 64)
			if err != nil {
				return err
			}

			pvCount, err := strconv.ParseUint(vg.PvCount, 10, 64)
			if err != nil {
				return err
			}
			lvCount, err := strconv.ParseUint(vg.LvCount, 10, 64)
			if err != nil {
				return err
			}
			snapCount, err := strconv.ParseUint(vg.SnapCount, 10, 64)
			if err != nil {
				return err
			}

			usedPercent := (float64(size) - float64(free)) / float64(size) * 100

			fields := map[string]interface{}{
				"size":                  size,
				"free":                  free,
				"used_percent":          usedPercent,
				"physical_volume_count": pvCount,
				"logical_volume_count":  lvCount,
				"snapshot_count":        snapCount,
			}

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

	return nil
}

func (lvm *LVM) gatherLogicalVolumes(acc telegraf.Accumulator) error {
	args := []string{
		"--reportformat", "json", "--units", "b", "--nosuffix",
		"-o", "lv_name,vg_name,lv_size,data_percent,metadata_percent",
	}
	out, err := lvm.runCmd(lvm.LVSBinary, args)
	if err != nil {
		return err
	}

	var report lvsReport
	err = json.Unmarshal(out, &report)
	if err != nil {
		return fmt.Errorf("failed to unmarshal logical vol JSON: %w", err)
	}

	if len(report.Report) > 0 {
		for _, lv := range report.Report[0].Lv {
			tags := map[string]string{
				"name":      lv.Name,
				"vol_group": lv.VolGroup,
			}

			size, err := strconv.ParseUint(lv.Size, 10, 64)
			if err != nil {
				return err
			}

			// Does not apply to all logical volumes, set default value
			if lv.DataPercent == "" {
				lv.DataPercent = "0.0"
			}
			dataPercent, err := strconv.ParseFloat(lv.DataPercent, 32)
			if err != nil {
				return err
			}

			// Does not apply to all logical volumes, set default value
			if lv.MetadataPercent == "" {
				lv.MetadataPercent = "0.0"
			}
			metadataPercent, err := strconv.ParseFloat(lv.MetadataPercent, 32)
			if err != nil {
				return err
			}

			fields := map[string]interface{}{
				"size":             size,
				"data_percent":     dataPercent,
				"metadata_percent": metadataPercent,
			}

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

	return nil
}

func (lvm *LVM) runCmd(cmd string, args []string) ([]byte, error) {
	execCmd := execCommand(cmd, args...)
	if lvm.UseSudo {
		execCmd = execCommand("sudo", append([]string{"-n", cmd}, args...)...)
	}

	out, err := internal.StdOutputTimeout(execCmd, 5*time.Second)
	if err != nil {
		return nil, fmt.Errorf(
			"failed to run command %s: %w - %s", strings.Join(execCmd.Args, " "), err, string(out),
		)
	}

	return out, nil
}

// Represents info about physical volume command, pvs, output
type pvsReport struct {
	Report []struct {
		Pv []struct {
			Name     string `json:"pv_name"`
			VolGroup string `json:"vg_name"`
			Size     string `json:"pv_size"`
			Free     string `json:"pv_free"`
			Used     string `json:"pv_used"`
		} `json:"pv"`
	} `json:"report"`
}

// Represents info about volume group command, vgs, output
type vgsReport struct {
	Report []struct {
		Vg []struct {
			Name      string `json:"vg_name"`
			Size      string `json:"vg_size"`
			Free      string `json:"vg_free"`
			LvCount   string `json:"lv_count"`
			PvCount   string `json:"pv_count"`
			SnapCount string `json:"snap_count"`
		} `json:"vg"`
	} `json:"report"`
}

// Represents info about logical volume command, lvs, output
type lvsReport struct {
	Report []struct {
		Lv []struct {
			Name            string `json:"lv_name"`
			VolGroup        string `json:"vg_name"`
			Size            string `json:"lv_size"`
			DataPercent     string `json:"data_percent"`
			MetadataPercent string `json:"metadata_percent"`
		} `json:"lv"`
	} `json:"report"`
}

func init() {
	inputs.Add("lvm", func() telegraf.Input {
		return &LVM{
			PVSBinary: "/usr/sbin/pvs",
			VGSBinary: "/usr/sbin/vgs",
			LVSBinary: "/usr/sbin/lvs",
		}
	})
}
