/*
** Zabbix
** Copyright (C) 2001-2023 Zabbix SIA
**
** This program is free software; you can redistribute it and/or modify
** it under the terms of the GNU General Public License as published by
** the Free Software Foundation; either version 2 of the License, or
** (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
** GNU General Public License for more details.
**
** You should have received a copy of the GNU General Public License
** along with this program; if not, write to the Free Software
** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
**/

package smart

import (
	"encoding/json"
	"fmt"
	"strings"

	"git.zabbix.com/ap/plugin-support/conf"
	"git.zabbix.com/ap/plugin-support/plugin"
	"git.zabbix.com/ap/plugin-support/zbxerr"
)

const (
	twoParameters = 2
	oneParameter  = 1
	all           = 0

	firstParameter  = 0
	secondParameter = 1

	diskGet            = "smart.disk.get"
	diskDiscovery      = "smart.disk.discovery"
	attributeDiscovery = "smart.attribute.discovery"
)

// Options -
type Options struct {
	plugin.SystemOptions `conf:"optional,name=System"`
	Timeout              int    `conf:"optional,range=1:30"`
	Path                 string `conf:"optional"`
}

// Plugin -
type Plugin struct {
	plugin.Base
	options Options
}

var impl Plugin

// Configure -
func (p *Plugin) Configure(global *plugin.GlobalOptions, options interface{}) {
	if err := conf.Unmarshal(options, &p.options); err != nil {
		p.Errf("cannot unmarshal configuration options: %s", err)
	}

	if p.options.Timeout == 0 {
		p.options.Timeout = global.Timeout
	}
}

// Validate -
func (p *Plugin) Validate(options interface{}) error {
	var o Options
	return conf.Unmarshal(options, &o)
}

// Export -
func (p *Plugin) Export(key string, params []string, ctx plugin.ContextProvider) (result interface{}, err error) {
	if len(params) > 0 && key != diskGet {
		return nil, zbxerr.ErrorTooManyParameters
	}

	if err = p.checkVersion(); err != nil {
		return
	}

	var jsonArray []byte

	switch key {
	case diskDiscovery:
		jsonArray, err = p.diskDiscovery()
		if err != nil {
			return nil, zbxerr.ErrorCannotFetchData.Wrap(err)
		}

	case diskGet:
		jsonArray, err = p.diskGet(params)
		if err != nil {
			return nil, zbxerr.ErrorCannotFetchData.Wrap(err)
		}

	case attributeDiscovery:
		jsonArray, err = p.attributeDiscovery()
		if err != nil {
			return nil, zbxerr.ErrorCannotFetchData.Wrap(err)
		}

	default:
		return nil, zbxerr.ErrorUnsupportedMetric
	}

	return string(jsonArray), nil
}

func (p *Plugin) diskDiscovery() (jsonArray []byte, err error) {
	out := []device{}

	r, err := p.execute(false)
	if err != nil {
		return nil, err
	}

	for _, dev := range r.devices {
		out = append(out, device{
			Name:         cutPrefix(dev.Info.Name),
			DeviceType:   getType(dev.Info.DevType, dev.RotationRate, dev.SmartAttributes.Table),
			Model:        dev.ModelName,
			SerialNumber: dev.SerialNumber,
			Path:         dev.Info.name,
			RaidType:     dev.Info.raidType,
			Attributes:   getAttributes(dev),
		})
	}

	jsonArray, err = json.Marshal(out)
	if err != nil {
		return nil, zbxerr.ErrorCannotMarshalJSON.Wrap(err)
	}

	return
}

func (p *Plugin) diskGet(params []string) ([]byte, error) {
	switch len(params) {
	case twoParameters:
		return p.diskGetSingle(params[firstParameter], params[secondParameter])
	case oneParameter:
		return p.diskGetSingle(params[firstParameter], "")
	case all:
		return p.diskGetAll()
	default:
		return nil, zbxerr.ErrorTooManyParameters
	}
}

func (p *Plugin) diskGetSingle(path, raidType string) ([]byte, error) {
	executable := path

	if raidType != "" {
		executable = fmt.Sprintf("%s -d %s", executable, raidType)
	}

	device, err := p.executeSingle(executable)
	if err != nil {
		return nil, err
	}

	out, err := setSingleDiskFields(device)
	if err != nil {
		return nil, err
	}

	jsonArray, err := json.Marshal(out)
	if err != nil {
		return nil, zbxerr.ErrorCannotMarshalJSON.Wrap(err)
	}

	return jsonArray, nil
}

func (p *Plugin) diskGetAll() (jsonArray []byte, err error) {
	r, err := p.execute(true)
	if err != nil {
		return nil, err
	}

	fields, err := setDiskFields(r.jsonDevices)
	if err != nil {
		return nil, err
	}

	if fields == nil {
		jsonArray, err = json.Marshal([]string{})
		if err != nil {
			return nil, zbxerr.ErrorCannotMarshalJSON.Wrap(err)
		}

		return
	}

	jsonArray, err = json.Marshal(fields)
	if err != nil {
		return nil, zbxerr.ErrorCannotMarshalJSON.Wrap(err)
	}

	return
}

func (p *Plugin) attributeDiscovery() (jsonArray []byte, err error) {
	out := []attribute{}

	r, err := p.execute(false)
	if err != nil {
		return nil, err
	}

	for _, dev := range r.devices {
		t := getAttributeType(dev.Info.DevType, dev.RotationRate, dev.SmartAttributes.Table)
		for _, attr := range dev.SmartAttributes.Table {
			out = append(
				out, attribute{
					Name:       cutPrefix(dev.Info.Name),
					DeviceType: t,
					ID:         attr.ID,
					Attrname:   attr.Attrname,
					Thresh:     attr.Thresh,
				})
		}
	}

	jsonArray, err = json.Marshal(out)
	if err != nil {
		return nil, zbxerr.ErrorCannotMarshalJSON.Wrap(err)
	}

	return
}

// setSingleDiskFields goes through provided device json data and sets required output fields.
// It returns an error if there is an issue with unmarshal for the provided input JSON map.
func setSingleDiskFields(dev []byte) (out map[string]interface{}, err error) {
	attr := make(map[string]interface{})
	if err = json.Unmarshal(dev, &attr); err != nil {
		return out, zbxerr.ErrorCannotUnmarshalJSON.Wrap(err)
	}

	var sd singleDevice
	if err = json.Unmarshal(dev, &sd); err != nil {
		return out, zbxerr.ErrorCannotUnmarshalJSON.Wrap(err)
	}

	diskType := getType(getTypeFromJson(attr), getRateFromJson(attr), getTablesFromJson(attr))

	out = map[string]interface{}{}
	out["disk_type"] = diskType
	out["firmware_version"] = sd.Firmware
	out["model_name"] = sd.ModelName
	out["serial_number"] = sd.SerialNumber
	out["exit_status"] = sd.Smartctl.ExitStatus

	var errors []string
	for _, msg := range sd.Smartctl.Messages {
		errors = append(errors, msg.Str)
	}

	out["error"] = strings.Join(errors, ", ")
	out["self_test_passed"] = setSelfTest(sd)

	if diskType == nvmeType {
		out["temperature"] = sd.HealthLog.Temperature
		out["power_on_time"] = sd.HealthLog.PowerOnTime
		out["critical_warning"] = sd.HealthLog.CriticalWarning
		out["media_errors"] = sd.HealthLog.MediaErrors
		out["percentage_used"] = sd.HealthLog.Percentage_used
	} else {
		out["temperature"] = sd.Temperature.Current
		out["power_on_time"] = sd.PowerOnTime.Hours
		out["critical_warning"] = 0
		out["media_errors"] = 0
		out["percentage_used"] = 0
	}

	for _, a := range sd.SmartAttributes.Table {
		if a.Name == unknownAttrName {
			continue
		}

		out[strings.ToLower(a.Name)] = singleRequestAttribute{a.Raw.Value, a.Raw.Str}
	}

	return
}

// setSelfTest determines if device is self test capable and if the test is passed.
func setSelfTest(sd singleDevice) *bool {
	if sd.Data.Capabilities.SelfTestsSupported {
		return &sd.Data.SelfTest.Status.Passed
	}

	return nil
}

// setDiskFields goes through provided device json map and sets disk_name
// disk_type and returns the devices in a slice.
// It returns an error if there is an issue with unmarshal for the provided input JSON map
func setDiskFields(deviceJsons map[string]jsonDevice) (out []interface{}, err error) {
	for k, v := range deviceJsons {
		b := make(map[string]interface{})
		if err = json.Unmarshal([]byte(v.jsonData), &b); err != nil {
			return out, zbxerr.ErrorCannotUnmarshalJSON.Wrap(err)
		}

		b["disk_name"] = cutPrefix(k)
		b["disk_type"] = getType(getTypeFromJson(b), getRateFromJson(b), getTablesFromJson(b))

		out = append(out, b)
	}

	return
}

func getRateFromJson(in map[string]interface{}) (out int) {
	if r, ok := in[rotationRateFieldName]; ok {
		switch rate := r.(type) {
		case int:
			out = rate
		case float64:
			out = int(rate)
		}
	}

	return
}

func getTypeFromJson(in map[string]interface{}) (out string) {
	if dev, ok := in[deviceFieldName]; ok {
		m, ok := dev.(map[string]interface{})
		if ok {
			if t, ok := m[typeFieldName]; ok {
				s, ok := t.(string)
				if ok {
					out = s
				}
			}
		}
	}

	return
}

func getTablesFromJson(in map[string]interface{}) (out []table) {
	attr, ok := in[ataSmartAttrFieldName]
	if !ok {
		return
	}

	a, ok := attr.(map[string]interface{})
	if !ok {
		return
	}

	tables, ok := a[ataSmartAttrTableFieldName]
	if !ok {
		return
	}

	tmp, ok := tables.([]interface{})
	if !ok {
		return
	}

	b, err := json.Marshal(tmp)
	if err != nil {
		return
	}

	err = json.Unmarshal(b, &out)
	if err != nil {
		return
	}

	return
}

func getAttributeType(devType string, rate int, tables []table) string {
	if devType == unknownType {
		return unknownType
	}

	return getTypeByRateAndAttr(rate, tables)
}

func getAttributes(in deviceParser) (out string) {
	for _, table := range in.SmartAttributes.Table {
		if table.Attrname == unknownAttrName {
			continue
		}

		out = out + " " + table.Attrname
	}

	return strings.TrimSpace(out)
}

func getType(devType string, rate int, tables []table) string {
	switch devType {
	case nvmeType:
		return nvmeType
	case unknownType:
		return unknownType
	default:
		return getTypeByRateAndAttr(rate, tables)
	}
}

func getTypeByRateAndAttr(rate int, tables []table) string {
	if rate > 0 {
		return hddType
	}

	for _, t := range tables {
		if t.Attrname == spinUpAttrName {
			return hddType
		}
	}

	return ssdType
}

func init() {
	plugin.RegisterMetrics(&impl, "Smart",
		"smart.disk.discovery", "Returns JSON array of smart devices.",
		"smart.disk.get", "Returns JSON data of smart device.",
		"smart.attribute.discovery", "Returns JSON array of smart device attributes.",
	)
}