package collectors

import (
	"bufio"
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"log"
	"os/exec"
	"strconv"
	"strings"
	"time"

	lp "github.com/ClusterCockpit/cc-lib/ccMessage"
	cclog "github.com/ClusterCockpit/cc-metric-collector/pkg/ccLogger"
)

const IPMISENSORS_PATH = `ipmi-sensors`

type IpmiCollectorConfig struct {
	ExcludeDevices  []string `json:"exclude_devices"`
	IpmitoolPath    string   `json:"ipmitool_path"`
	IpmisensorsPath string   `json:"ipmisensors_path"`
	ExcludeMetrics  []string `json:"exclude_metrics,omitempty"`
	OnlyMetrics     []string `json:"only_metrics,omitempty"`
}

type IpmiCollector struct {
	metricCollector
	config      IpmiCollectorConfig
	ipmitool    string
	ipmisensors string
}

// shouldOutput checks whether a metric should be forwarded based on only_metrics/exclude_metrics.
func (m *IpmiCollector) shouldOutput(metricName string) bool {
	if len(m.config.OnlyMetrics) > 0 {
		for _, name := range m.config.OnlyMetrics {
			if name == metricName {
				return true
			}
		}
		return false
	}
	for _, name := range m.config.ExcludeMetrics {
		if name == metricName {
			return false
		}
	}
	return true
}

func (m *IpmiCollector) Init(config json.RawMessage) error {
	if m.init {
		return nil
	}

	m.name = "IpmiCollector"
	m.setup()
	m.parallel = true
	m.meta = map[string]string{
		"source": m.name,
		"group":  "IPMI",
	}
	// Default paths:
	m.config.IpmitoolPath = "ipmitool"
	m.config.IpmisensorsPath = "ipmi-sensors"
	if len(config) > 0 {
		err := json.Unmarshal(config, &m.config)
		if err != nil {
			return err
		}
	}
	// Check ipmitool: test with "-V" to verify its existence.
	p, err := exec.LookPath(m.config.IpmitoolPath)
	if err == nil {
		command := exec.Command(p, "-V")
		err := command.Run()
		if err != nil {
			cclog.ComponentError(m.name, fmt.Sprintf("Failed to execute %s -V: %v", p, err.Error()))
			m.ipmitool = ""
		} else {
			m.ipmitool = p
		}
	}
	// Check ipmi-sensors executable.
	p, err = exec.LookPath(m.config.IpmisensorsPath)
	if err == nil {
		command := exec.Command(p)
		err := command.Run()
		if err != nil {
			cclog.ComponentError(m.name, fmt.Sprintf("Failed to execute %s: %v", p, err.Error()))
			m.ipmisensors = ""
		} else {
			m.ipmisensors = p
		}
	}
	if len(m.ipmitool) == 0 && len(m.ipmisensors) == 0 {
		return errors.New("no usable IPMI reader found")
	}

	m.init = true
	return nil
}

func (m *IpmiCollector) readIpmiTool(cmd string, output chan lp.CCMessage) {
	command := exec.Command(cmd, "sensor")
	stdout, _ := command.StdoutPipe()
	errBuf := new(bytes.Buffer)
	command.Stderr = errBuf

	if err := command.Start(); err != nil {
		cclog.ComponentError(m.name, fmt.Sprintf("readIpmiTool(): Failed to start command \"%s\": %v", command.String(), err))
		return
	}

	scanner := bufio.NewScanner(stdout)
	for scanner.Scan() {
		lv := strings.Split(scanner.Text(), "|")
		if len(lv) < 3 {
			continue
		}
		v, err := strconv.ParseFloat(strings.TrimSpace(lv[1]), 64)
		if err == nil {
			name := strings.ToLower(strings.Replace(strings.TrimSpace(lv[0]), " ", "_", -1))
			if !m.shouldOutput(name) {
				continue
			}
			unit := strings.TrimSpace(lv[2])
			// Standardize unit names.
			if unit == "Volts" {
				unit = "Volts"
			} else if unit == "degrees C" {
				unit = "degC"
			} else if unit == "degrees F" {
				unit = "degF"
			} else if unit == "Watts" {
				unit = "Watts"
			}
			y, err := lp.NewMessage(name, map[string]string{"type": "node"}, m.meta, map[string]interface{}{"value": v}, time.Now())
			if err == nil {
				y.AddMeta("unit", unit)
				output <- y
			}
		}
	}

	if err := command.Wait(); err != nil {
		errMsg, _ := io.ReadAll(errBuf)
		cclog.ComponentError(m.name, fmt.Sprintf("readIpmiTool(): Failed to wait for command \"%s\": %v\n", command.String(), err))
		cclog.ComponentError(m.name, fmt.Sprintf("readIpmiTool(): command stderr: \"%s\"\n", strings.TrimSpace(string(errMsg))))
		return
	}
}

func (m *IpmiCollector) readIpmiSensors(cmd string, output chan lp.CCMessage) {
	command := exec.Command(cmd, "--comma-separated-output", "--sdr-cache-recreate")
	command.Wait()
	stdout, err := command.Output()
	if err != nil {
		log.Print(err)
		return
	}

	ll := strings.Split(string(stdout), "\n")
	for _, line := range ll {
		lv := strings.Split(line, ",")
		if len(lv) > 3 {
			v, err := strconv.ParseFloat(lv[3], 64)
			if err == nil {
				name := strings.ToLower(strings.Replace(lv[1], " ", "_", -1))
				if !m.shouldOutput(name) {
					continue
				}
				y, err := lp.NewMessage(name, map[string]string{"type": "node"}, m.meta, map[string]interface{}{"value": v}, time.Now())
				if err == nil {
					if len(lv) > 4 {
						y.AddMeta("unit", lv[4])
					}
					output <- y
				}
			}
		}
	}
}

func (m *IpmiCollector) Read(interval time.Duration, output chan lp.CCMessage) {
	if !m.init {
		return
	}

	if len(m.config.IpmitoolPath) > 0 {
		m.readIpmiTool(m.config.IpmitoolPath, output)
	} else if len(m.config.IpmisensorsPath) > 0 {
		m.readIpmiSensors(m.config.IpmisensorsPath, output)
	}
}

func (m *IpmiCollector) Close() {
	m.init = false
}