// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//go:build linux

package sysfs

import (
	"fmt"
	"os"
	"path/filepath"
	"strconv"

	"github.com/prometheus/procfs/internal/util"
)

// Documentation of the sysfs path
// https://docs.nvidia.com/networking/display/mlnxofedv571020/explicit+congestion+notification+(ecn)
// https://enterprise-support.nvidia.com/s/article/dcqcn-parameters

// Ecn contains values from /sys/class/net/<iface>/ecn/roce_np/
// for single interface (iface).
type RoceNpEcn struct {
	// A map from a priority to it's enabled status
	Ecn map[uint8]bool
	// Minimum time between sending CNPs from the port, in microseconds.
	// Range: 0-4095, Default: 4
	MinTimeBetweenCnps uint64
	// The DSCP value for CNPs.
	// Range: 0-63, Default: 48
	CnpDscp uint64
	// The PCP value for CNPs.
	// Range: 0-7, Default: 6
	Cnp802pPriority uint64
}

// Ecn contains values from /sys/class/net/<iface>/ecn/roce_rp/
// for single interface (iface).
type RoceRpEcn struct {
	// A map from a priority to it's enabled status
	Ecn map[uint8]bool

	// Alpha Update

	// Every Alpha Update Period alpha is updated.
	// If the CNP is received during this period, alpha is incremented.
	// Otherwise, it is decremented.
	// Range: 0-1023, Default: 1019
	DceTCPG uint64
	// The Alpha Update Period used in the formula for DceTCPG. Unit is microseconds.
	// Range: 1-131071, Default: 1
	DceTCPRtt uint64
	// This parameter sets the initial value of alpha that should be used when receiving
	// the first CNP for a flow. Fixed point with 10 bits in the fraction part.
	// Range: 1-1023, Default: 1023
	InitialAlphaValue uint64

	// Rate Decrease

	// Rates (current, target) on first CNP (0 – 85% of line rate) in Mbps.
	// Range: 0, 1-line rate, Default: 0
	RateToSetOnFirstCnp uint64
	// This parameter defines the maximal ratio of rate decrease in a single event.
	// Range: 0-100, Default: 50
	RpgMinDecFac uint64
	// This parameter defines the minimal rate limit of the QP in Mbps.
	// Range: 1-line rate, Default: 1
	RpgMinRate uint64
	// The coefficient between alpha and the rate reduction factor.
	// Range: 10-11, Default: 11
	RpgGd uint64
	// The time period between rate reductions in microseconds.
	// Range: 0-UINT32, Default: 4
	RateReduceMonitorPeriod uint64

	// Rate Increase

	// If set, every rate decreases. The target rate is updated to the current rate.
	// Otherwise, the target rate is updated to the current rate only on the first
	// decrement after the increment event.
	ClampTgtRate bool
	// The time period between rate increase events in microseconds.
	// Range: 1-131071, Default: 300
	RpgTimeReset uint64
	// The sent bytes counter between rate increase events.
	// Range: 1-32767, Default: 32767
	RpgByteReset uint64
	// The threshold of rate increase events for moving to next rate increase phase.
	// Range: 1-31, Default: 1
	RpgThreshold uint64
	// The rate increase value in the Additive Increase phase in Mbps.
	// Range: 1-line rate, Default: 5
	RpgAiRate uint64
	// The rate increase value in the Hyper Increase phase in Mbps.
	// Range: 1-line rate, Default: 1
	RpgHaiRate uint64
}

// EcnIface contains Ecn info from files in /sys/class/net/<iface>/ecn/
// for single interface (iface).
type EcnIface struct {
	Name string // Interface name
	// protocols
	RoceNpEcn RoceNpEcn // Notification point
	RoceRpEcn RoceRpEcn // Reaction point
}

// AllEcnIface is collection of Ecn info for every interface (iface) in /sys/class/net.
// The map keys are interface (iface) names.
type AllEcnIface map[string]EcnIface

// EcnByIface returns info for a single net interfaces (iface).
func (fs FS) EcnByIface(devicePath string) (*EcnIface, error) {
	_, err := fs.NetClassByIface(devicePath)
	if err != nil {
		return nil, err
	}

	path := fs.sys.Path(netclassPath)
	ecnPath := filepath.Join(path, devicePath, "ecn")
	validPath, err := PathExistsAndIsDir(ecnPath)
	if err != nil {
		return nil, err
	}
	if !validPath {
		// this device doesn't have ECN values at this path
		return nil, fmt.Errorf("does not have ECN values: %q", devicePath)
	}

	ecnIface, err := ParseEcnIfaceInfo(ecnPath)
	if err != nil {
		return nil, err
	}
	ecnIface.Name = devicePath

	return ecnIface, nil
}

// EcnDevices returns EcnIface for all net interfaces (iface) read from /sys/class/net/<iface>/ecn.
func (fs FS) EcnDevices() (AllEcnIface, error) {
	devices, err := fs.NetClassDevices()
	if err != nil {
		return nil, err
	}

	path := fs.sys.Path(netclassPath)
	allEcnIface := AllEcnIface{}
	for _, devicePath := range devices {
		ecnPath := filepath.Join(path, devicePath, "ecn")
		validPath, err := PathExistsAndIsDir(ecnPath)
		if err != nil {
			return nil, err
		}
		if !validPath {
			// this device doesn't have ECN values at this path
			continue
		}
		ecnIface, err := ParseEcnIfaceInfo(ecnPath)
		if err != nil {
			return nil, err
		}
		ecnIface.Name = devicePath
		allEcnIface[devicePath] = *ecnIface
	}

	return allEcnIface, nil
}

// ParseEcnIfaceInfo scans predefined files in /sys/class/net/<iface>/ecn
// directory and gets their contents.
func ParseEcnIfaceInfo(ecnPath string) (*EcnIface, error) {
	ecnIface := EcnIface{}
	err := ParseRoceNpEcnInfo(filepath.Join(ecnPath, "roce_np"), &ecnIface.RoceNpEcn)
	if err != nil {
		return nil, err
	}

	err = ParseRoceRpEcnInfo(filepath.Join(ecnPath, "roce_rp"), &ecnIface.RoceRpEcn)
	if err != nil {
		return nil, err
	}

	return &ecnIface, nil
}

// ParseEcnIfaceInfo scans predefined files in /sys/class/net/<iface>/ecn/roce_np/
// directory and gets their contents.
func ParseRoceNpEcnInfo(ecnPath string, ecn *RoceNpEcn) error {
	value, err := ParseEcnEnable(filepath.Join(ecnPath, "enable"))
	if err != nil {
		return err
	}
	ecn.Ecn = value

	files, err := os.ReadDir(ecnPath)
	if err != nil {
		return err
	}

	for _, f := range files {
		if !f.Type().IsRegular() {
			continue
		}
		if err := ParseRoceNpEcnAttribute(ecnPath, f.Name(), ecn); err != nil {
			return err
		}
	}
	return nil
}

// Parses all of the attributes in for ROCE NP protocol.
func ParseRoceNpEcnAttribute(ecnPath string, attrName string, ecn *RoceNpEcn) error {
	attrPath := filepath.Join(ecnPath, attrName)
	value, err := util.SysReadFile(attrPath)
	if err != nil {
		if canIgnoreError(err) {
			return nil
		}
		return fmt.Errorf("failed to read file %q: %w", attrPath, err)
	}

	vp := util.NewValueParser(value)
	switch attrName {
	case "min_time_between_cnps":
		ecn.MinTimeBetweenCnps = *vp.PUInt64()
	case "cnp_802p_prio":
		ecn.Cnp802pPriority = *vp.PUInt64()
	case "cnp_dscp":
		ecn.CnpDscp = *vp.PUInt64()
	default:
		return nil
	}

	return nil
}

// ParseRoceRpEcnInfo scans predefined files in /sys/class/net/<iface>/ecn/roce_rp/
// directory and gets their contents.
func ParseRoceRpEcnInfo(ecnPath string, ecn *RoceRpEcn) error {
	value, err := ParseEcnEnable(filepath.Join(ecnPath, "enable"))
	if err != nil {
		return err
	}
	ecn.Ecn = value

	files, err := os.ReadDir(ecnPath)
	if err != nil {
		return err
	}

	for _, f := range files {
		if !f.Type().IsRegular() {
			continue
		}
		if err := ParseRoceRpEcnAttribute(ecnPath, f.Name(), ecn); err != nil {
			return err
		}
	}
	return nil
}

// Parses all of the attributes in for ROCE RP protocol.
func ParseRoceRpEcnAttribute(ecnPath string, attrName string, ecn *RoceRpEcn) error {
	attrPath := filepath.Join(ecnPath, attrName)
	value, err := util.SysReadFile(attrPath)
	if err != nil {
		if canIgnoreError(err) {
			return nil
		}
		return fmt.Errorf("failed to read file %q: %w", attrPath, err)
	}

	vp := util.NewValueParser(value)
	switch attrName {
	case "clamp_tgt_rate":
		switch *vp.PUInt64() {
		case 0:
			ecn.ClampTgtRate = false
		case 1:
			ecn.ClampTgtRate = true
		default:
			return fmt.Errorf("failed to parse file %q: %w", attrPath, err)
		}
	case "dce_tcp_g":
		ecn.DceTCPG = *vp.PUInt64()
	case "dce_tcp_rtt":
		ecn.DceTCPRtt = *vp.PUInt64()
	case "initial_alpha_value":
		ecn.InitialAlphaValue = *vp.PUInt64()
	case "rate_reduce_monitor_period":
		ecn.RateReduceMonitorPeriod = *vp.PUInt64()
	case "rate_to_set_on_first_cnp":
		ecn.RateToSetOnFirstCnp = *vp.PUInt64()
	case "rpg_ai_rate":
		ecn.RpgAiRate = *vp.PUInt64()
	case "rpg_byte_reset":
		ecn.RpgByteReset = *vp.PUInt64()
	case "rpg_gd":
		ecn.RpgGd = *vp.PUInt64()
	case "rpg_hai_rate":
		ecn.RpgHaiRate = *vp.PUInt64()
	case "rpg_min_dec_fac":
		ecn.RpgMinDecFac = *vp.PUInt64()
	case "rpg_min_rate":
		ecn.RpgMinRate = *vp.PUInt64()
	case "rpg_threshold":
		ecn.RpgThreshold = *vp.PUInt64()
	case "rpg_time_reset":
		ecn.RpgTimeReset = *vp.PUInt64()
	default:
		return nil
	}

	return nil
}

// parses the ECN enable directory. It takes a path which should be a directory.
// This directory should have filenames that are uint8 and the content of the file is
// either 0 or 1.
func ParseEcnEnable(path string) (map[uint8]bool, error) {
	// Read the files in the directory
	files, err := os.ReadDir(path)
	if err != nil {
		return nil, err
	}

	ecn := make(map[uint8]bool)
	// Iterate through each file in the directory
	for _, file := range files {
		// Only process files (skip directories)
		if file.IsDir() {
			continue
		}

		// Extract the file name (which should be the integer key)
		filename := file.Name()

		// Attempt to convert the file name to an integer
		filenameInt, err := strconv.ParseUint(filename, 10, 8)
		if err != nil {
			// Skip the file if the name cannot be converted to an integer
			continue
		}

		value, err := util.SysReadFile(filepath.Join(path, filename))
		if err != nil {
			if canIgnoreError(err) {
				return nil, err
			}
			return nil, fmt.Errorf("failed to read file %q: %w", filename, err)
		}

		vp := util.NewValueParser(value)
		fileValue := *vp.PUInt64()
		switch fileValue {
		case 0:
			ecn[uint8(filenameInt)] = false
		case 1:
			ecn[uint8(filenameInt)] = true
		default:
			return nil, fmt.Errorf("failed to parse file %q: %q", filename, value)
		}
	}

	return ecn, nil
}

// Utility function that given a path will return if the path is a dir or not.
func PathExistsAndIsDir(path string) (bool, error) {
	info, err := os.Stat(path)
	if err != nil {
		if os.IsNotExist(err) {
			return false, nil // Path does not exist
		}
		return false, err // Some other error occurred
	}
	return info.IsDir(), nil // Check if the path is a directory
}
