// Copyright 2021 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.

// +build linux
// +build !nofibrechannel

package collector

import (
	"fmt"
	"os"

	"github.com/go-kit/kit/log"
	"github.com/go-kit/kit/log/level"
	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/procfs/sysfs"
)

const maxUint64 = ^uint64(0)

type fibrechannelCollector struct {
	fs          sysfs.FS
	metricDescs map[string]*prometheus.Desc
	logger      log.Logger
	subsystem   string
}

func init() {
	registerCollector("fibrechannel", defaultEnabled, NewFibreChannelCollector)
}

// NewFibreChannelCollector returns a new Collector exposing FibreChannel stats.
func NewFibreChannelCollector(logger log.Logger) (Collector, error) {
	var i fibrechannelCollector
	var err error

	i.fs, err = sysfs.NewFS(*sysPath)
	if err != nil {
		return nil, fmt.Errorf("failed to open sysfs: %w", err)
	}
	i.logger = logger

	// Detailed description for all metrics.
	descriptions := map[string]string{
		"dumped_frames_total":            "Number of dumped frames",
		"loss_of_signal_total":           "Number of times signal has been lost",
		"loss_of_sync_total":             "Number of failures on either bit or transmission word boundaries",
		"rx_frames_total":                "Number of frames received",
		"error_frames_total":             "Number of errors in frames",
		"invalid_tx_words_total":         "Number of invalid words transmitted by host port",
		"seconds_since_last_reset_total": "Number of seconds since last host port reset",
		"tx_words_total":                 "Number of words transmitted by host port",
		"invalid_crc_total":              "Invalid Cyclic Redundancy Check count",
		"nos_total":                      "Number Not_Operational Primitive Sequence received by host port",
		"fcp_packet_aborts_total":        "Number of aborted packets",
		"rx_words_total":                 "Number of words received by host port",
		"tx_frames_total":                "Number of frames transmitted by host port",
		"link_failure_total":             "Number of times the host port link has failed",
		"name":                           "Name of Fibre Channel HBA",
		"speed":                          "Current operating speed",
		"port_state":                     "Current port state",
		"port_type":                      "Port type, what the port is connected to",
		"symbolic_name":                  "Symoblic Name",
		"node_name":                      "Node Name as hexadecimal string",
		"port_id":                        "Port ID as string",
		"port_name":                      "Port Name as hexadecimal string",
		"fabric_name":                    "Fabric Name; 0 if PTP",
		"dev_loss_tmo":                   "Device Loss Timeout in seconds",
		"supported_classes":              "The FC classes supported",
		"supported_speeds":               "The FC speeds supported",
	}

	i.metricDescs = make(map[string]*prometheus.Desc)
	i.subsystem = "fibrechannel"

	for metricName, description := range descriptions {
		i.metricDescs[metricName] = prometheus.NewDesc(
			prometheus.BuildFQName(namespace, i.subsystem, metricName),
			description,
			[]string{"fc_host"},
			nil,
		)
	}

	return &i, nil
}

func (c *fibrechannelCollector) pushMetric(ch chan<- prometheus.Metric, name string, value uint64, host string, valueType prometheus.ValueType) {
	ch <- prometheus.MustNewConstMetric(c.metricDescs[name], valueType, float64(value), host)
}

func (c *fibrechannelCollector) pushCounter(ch chan<- prometheus.Metric, name string, value uint64, host string) {
	// Don't push counters that aren't implemented (a counter equal to maxUint64 is unimplemented by the HBA firmware)
	if value != maxUint64 {
		c.pushMetric(ch, name, value, host, prometheus.CounterValue)
	}
}

func (c *fibrechannelCollector) Update(ch chan<- prometheus.Metric) error {
	hosts, err := c.fs.FibreChannelClass()
	if err != nil {
		if os.IsNotExist(err) {
			level.Debug(c.logger).Log("msg", "fibrechannel statistics not found, skipping")
			return ErrNoData
		}
		return fmt.Errorf("error obtaining FibreChannel class info: %s", err)
	}

	for _, host := range hosts {
		infoDesc := prometheus.NewDesc(
			prometheus.BuildFQName(namespace, c.subsystem, "info"),
			"Non-numeric data from /sys/class/fc_host/<host>, value is always 1.",
			[]string{"fc_host", "speed", "port_state", "port_type", "port_id", "port_name", "fabric_name", "symbolic_name", "supported_classes", "supported_speeds", "dev_loss_tmo"},
			nil,
		)
		infoValue := 1.0

		// First push the Host values
		ch <- prometheus.MustNewConstMetric(infoDesc, prometheus.GaugeValue, infoValue, host.Name, host.Speed, host.PortState, host.PortType, host.PortID, host.PortName, host.FabricName, host.SymbolicName, host.SupportedClasses, host.SupportedSpeeds, host.DevLossTMO)

		// Then the counters
		c.pushCounter(ch, "dumped_frames_total", host.Counters.DumpedFrames, host.Name)
		c.pushCounter(ch, "error_frames_total", host.Counters.ErrorFrames, host.Name)
		c.pushCounter(ch, "invalid_crc_total", host.Counters.InvalidCRCCount, host.Name)
		c.pushCounter(ch, "rx_frames_total", host.Counters.RXFrames, host.Name)
		c.pushCounter(ch, "rx_words_total", host.Counters.RXWords, host.Name)
		c.pushCounter(ch, "tx_frames_total", host.Counters.TXFrames, host.Name)
		c.pushCounter(ch, "tx_words_total", host.Counters.TXWords, host.Name)
		c.pushCounter(ch, "seconds_since_last_reset_total", host.Counters.SecondsSinceLastReset, host.Name)
		c.pushCounter(ch, "invalid_tx_words_total", host.Counters.InvalidTXWordCount, host.Name)
		c.pushCounter(ch, "link_failure_total", host.Counters.LinkFailureCount, host.Name)
		c.pushCounter(ch, "loss_of_sync_total", host.Counters.LossOfSyncCount, host.Name)
		c.pushCounter(ch, "loss_of_signal_total", host.Counters.LossOfSignalCount, host.Name)
		c.pushCounter(ch, "nos_total", host.Counters.NosCount, host.Name)
		c.pushCounter(ch, "fcp_packet_aborts_total", host.Counters.FCPPacketAborts, host.Name)
	}

	return nil
}
