package collectors

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

	lp "github.com/ClusterCockpit/cc-lib/ccMessage"
)

// First part contains the code for the general NfsCollector.
// Later, the general NfsCollector is more limited to Nfs3- and Nfs4Collector.

const NFSSTAT_EXEC = `nfsstat`

type NfsCollectorData struct {
	current int64
	last    int64
}

type nfsCollector struct {
	metricCollector
	tags    map[string]string
	version string
	config  struct {
		Nfsstats           string   `json:"nfsstat"`
		ExcludeMetrics     []string `json:"exclude_metrics,omitempty"`
		OnlyMetrics        []string `json:"only_metrics,omitempty"`
		SendAbsoluteValues bool     `json:"send_abs_values,omitempty"`
		SendDiffValues     bool     `json:"send_diff_values,omitempty"`
		SendDerivedValues  bool     `json:"send_derived_values,omitempty"`
	}
	data map[string]NfsCollectorData
}

func (m *nfsCollector) initStats() error {
	cmd := exec.Command(m.config.Nfsstats, `-l`, `--all`)
	cmd.Wait()
	buffer, err := cmd.Output()
	if err == nil {
		for _, line := range strings.Split(string(buffer), "\n") {
			lf := strings.Fields(line)
			if len(lf) != 5 {
				continue
			}
			if lf[1] == m.version {
				// Use the base metric name (without prefix) for filtering.
				name := strings.Trim(lf[3], ":")
				if _, exist := m.data[name]; !exist {
					value, err := strconv.ParseInt(lf[4], 0, 64)
					if err == nil {
						m.data[name] = NfsCollectorData{current: value, last: value}
					}
				}
			}
		}
	}
	return err
}

func (m *nfsCollector) updateStats() error {
	cmd := exec.Command(m.config.Nfsstats, `-l`, `--all`)
	cmd.Wait()
	buffer, err := cmd.Output()
	if err == nil {
		for _, line := range strings.Split(string(buffer), "\n") {
			lf := strings.Fields(line)
			if len(lf) != 5 {
				continue
			}
			if lf[1] == m.version {
				name := strings.Trim(lf[3], ":")
				if _, exist := m.data[name]; exist {
					value, err := strconv.ParseInt(lf[4], 0, 64)
					if err == nil {
						x := m.data[name]
						x.last = x.current
						x.current = value
						m.data[name] = x
					}
				}
			}
		}
	}
	return err
}

// shouldOutput returns true if a metric (base name + variant) should be forwarded.
// The variant is "" for absolute, "_diff" for diff, and "_rate" for derived.
// If only_metrics is set, only the metric names that exactly match (base+variant) are forwarded.
func (m *nfsCollector) shouldOutput(baseName, variant string) bool {
	finalName := baseName + variant
	if len(m.config.OnlyMetrics) > 0 {
		for _, n := range m.config.OnlyMetrics {
			if n == finalName {
				return true
			}
		}
		return false
	}
	for _, n := range m.config.ExcludeMetrics {
		if n == finalName {
			return false
		}
	}
	return true
}

func (m *nfsCollector) MainInit(config json.RawMessage) error {
	m.config.Nfsstats = NFSSTAT_EXEC
	if len(config) > 0 {
		err := json.Unmarshal(config, &m.config)
		if err != nil {
			return err
		}
	}
	// For backwards compatibility, send_abs_values defaults to true.
	if !m.config.SendAbsoluteValues {
		m.config.SendAbsoluteValues = true
	}
	// Diff and derived default to false if not specified.
	m.meta = map[string]string{"source": m.name, "group": "NFS"}
	m.tags = map[string]string{"type": "node"}
	_, err := exec.LookPath(m.config.Nfsstats)
	if err != nil {
		return fmt.Errorf("NfsCollector.Init(): Failed to find nfsstat binary '%s': %v", m.config.Nfsstats, err)
	}
	m.data = make(map[string]NfsCollectorData)
	m.initStats()
	m.init = true
	m.parallel = true
	return nil
}

func (m *nfsCollector) Read(interval time.Duration, output chan lp.CCMessage) {
	if !m.init {
		return
	}
	timestamp := time.Now()
	m.updateStats()
	prefix := ""
	switch m.version {
	case "v3":
		prefix = "nfs3"
	case "v4":
		prefix = "nfs4"
	default:
		prefix = "nfs"
	}
	for name, data := range m.data {
		// For each metric, apply filtering based on the final output name.
		// Absolute metric: final name = prefix + "_" + name
		if m.config.SendAbsoluteValues && m.shouldOutput(name, "") {
			absMsg, err := lp.NewMessage(fmt.Sprintf("%s_%s", prefix, name), m.tags, m.meta, map[string]interface{}{"value": data.current}, timestamp)
			if err == nil {
				absMsg.AddMeta("version", m.version)
				output <- absMsg
			}
		}
		if m.config.SendDiffValues && m.shouldOutput(name, "_diff") {
			diff := data.current - data.last
			diffMsg, err := lp.NewMessage(fmt.Sprintf("%s_%s_diff", prefix, name), m.tags, m.meta, map[string]interface{}{"value": diff}, timestamp)
			if err == nil {
				diffMsg.AddMeta("version", m.version)
				output <- diffMsg
			}
		}
		if m.config.SendDerivedValues && m.shouldOutput(name, "_rate") {
			diff := data.current - data.last
			rate := float64(diff) / interval.Seconds()
			derivedMsg, err := lp.NewMessage(fmt.Sprintf("%s_%s_rate", prefix, name), m.tags, m.meta, map[string]interface{}{"value": rate}, timestamp)
			if err == nil {
				derivedMsg.AddMeta("version", m.version)
				output <- derivedMsg
			}
		}
	}
}

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

type Nfs3Collector struct {
	nfsCollector
}

type Nfs4Collector struct {
	nfsCollector
}

func (m *Nfs3Collector) Init(config json.RawMessage) error {
	m.name = "Nfs3Collector"
	m.version = "v3"
	m.setup()
	return m.MainInit(config)
}

func (m *Nfs4Collector) Init(config json.RawMessage) error {
	m.name = "Nfs4Collector"
	m.version = "v4"
	m.setup()
	return m.MainInit(config)
}