package main

import (
	"flag"
	"net/http"
	"github.com/prometheus/client_golang/prometheus/promhttp"
	"fmt"
	"os"
	"github.com/prometheus/common/log"
	"github.com/prometheus/common/version"
	"github.com/prometheus/client_golang/prometheus"
	"sync"
	"crypto/tls"
	"io/ioutil"
	"encoding/json"
	"bytes"
	"strconv"
	"strings"
)

const (
	namespace = "nexenta" // For Prometheus metrics.
	apiPath   = "/rest/nms"
)

var (
	listenAddr      = flag.String("listen-address", ":9457", "The address to listen on for HTTP requests.")
	metricsEndpoint = flag.String("metrics-endpoint", "/metrics", "Path under which to expose metrics.")
	apiHost         = flag.String("host", "nexenta", "Nexenta API host.")
	apiPort         = flag.String("port", "8457", "Nexenta API port.")
	apiUser         = flag.String("user", "admin", "Nexenta API username.")
	apiPass         = flag.String("password", "password", "Nexenta API password.")
	apiSsl          = flag.Bool("ssl", false, "Use SSL for the Nexenta API.")
	insecure        = flag.Bool("insecure", false, "Ignore server certificate if using https.")
	showVersion     = flag.Bool("version", false, "Print version information.")
)

type ApiRequest struct {
	Object  string   `json:"object"`
	Method  string   `json:"method"`
	Params  []string `json:"params"`
}

type ApiResponse struct {
	TGFlash string      `json:"tg_flash"`
	Result  interface{} `json:"result"`
	Error   ApiError    `json:"error"`
}

type ApiError struct {
	Message string `json:"message"`
}

type VolumeStatus struct {
	State  []string `json:"state"`
	Errors []string `json:"errors"`
}

type Sensor struct {
	Name  string `json:"name"`
	Value string `json:"value"`
	State string `json:"state"`
	Dev   string `json:"dev"`
	Units string `json:"units"`
	Type  string `json:"type"`
}

type LicenseInfo struct {
	DaysLeft string `json:"license_days_left"`
}

type Exporter struct {
	URI    string
	mutex  sync.Mutex
	client *http.Client

	apiReachable float64

	up             	*prometheus.Desc
	scrapeFailures 	 prometheus.Counter
	volumeOnline   	*prometheus.Desc
	jbodStatus	   	*prometheus.Desc
	jbodTemp       	*prometheus.Desc
	jbodVoltage	   	*prometheus.Desc
	jbodSlotStatus 	*prometheus.Desc
	licenseDaysLeft	*prometheus.Desc
}

func NewExporter(uri string) *Exporter {
	return &Exporter{
		URI: uri,
		up: prometheus.NewDesc(
			prometheus.BuildFQName(namespace, "", "up"),
			"Is the Nexenta API reachable",
			nil,
			prometheus.Labels{"host":*apiHost},
		),
		scrapeFailures: prometheus.NewCounter(prometheus.CounterOpts{
			Namespace:   namespace,
			Name:        "exporter_scrape_failures_total",
			Help:        "Number of errors while scraping the Nexenta API",
			ConstLabels: prometheus.Labels{"host":*apiHost},
		}),
		volumeOnline: prometheus.NewDesc(
			prometheus.BuildFQName(namespace, "volume", "online"),
			"Status of volume.",
			[]string{"volume"},
			prometheus.Labels{"host":*apiHost},
		),
		jbodStatus: prometheus.NewDesc(
			prometheus.BuildFQName(namespace, "jbod", "status"),
			"Status of JBOD.",
			[]string{"jbod"},
			prometheus.Labels{"host":*apiHost},
		),
		jbodTemp: prometheus.NewDesc(
			prometheus.BuildFQName(namespace, "jbod", "temp"),
			"Temperature of JBOD.",
			[]string{"jbod", "name", "state"},
			prometheus.Labels{"host":*apiHost},
		),
		jbodVoltage: prometheus.NewDesc(
			prometheus.BuildFQName(namespace, "jbod", "voltage"),
			"Voltage of JBOD.",
			[]string{"jbod", "name", "state"},
			prometheus.Labels{"host":*apiHost},
		),
		jbodSlotStatus: prometheus.NewDesc(
			prometheus.BuildFQName(namespace, "jbod", "slot_status"),
			"Status of JBOD slot.",
			[]string{"jbod", "slot", "state"},
			prometheus.Labels{"host":*apiHost},
		),
		licenseDaysLeft: prometheus.NewDesc(
			prometheus.BuildFQName(namespace, "license", "days_left"),
			"License days left.",
			nil,
			prometheus.Labels{"host":*apiHost},
		),
		client: &http.Client{
			Transport: &http.Transport{
				TLSClientConfig: &tls.Config{InsecureSkipVerify: *insecure},
			},
		},
	}
}

func (e *Exporter) Describe(ch chan<- *prometheus.Desc) {
	ch <- e.up
	e.scrapeFailures.Describe(ch)
	ch <- e.volumeOnline
	ch <- e.jbodStatus
	ch <- e.jbodTemp
	ch <- e.jbodVoltage
	ch <- e.jbodSlotStatus
}

func (e *Exporter) Collect(ch chan<- prometheus.Metric) {
	e.mutex.Lock() // To protect metrics from concurrent collects.
	defer e.mutex.Unlock()
	err := e.collect(ch)
	if err != nil {
		log.Errorf("%v", err)
	}

	ch <- prometheus.MustNewConstMetric(e.up, prometheus.GaugeValue, e.apiReachable)

	return
}

func (e *Exporter) collect(ch chan<- prometheus.Metric) error {

	// Volumes
	volumes, err := e.getVolumes(ch)
	if err != nil {
		err = fmt.Errorf("getVolumes(): %v", err)
	}

	if len(volumes) > 0 {
		for _, volume := range volumes {
			err = e.getVolumeStatus(ch, volume)
			if err != nil {
				err = fmt.Errorf("getVolumeStatus(): %v", err)
			}
		}
	}

	// JBODs
	jbods, err := e.getJBODs(ch)
	if err != nil {
		err = fmt.Errorf("getJBODs(): %v", err)
	}

	if len(jbods) > 0 {
		for _, jbod := range jbods {
			err = e.getJBODSensors(ch, jbod)
			if err != nil {
				err = fmt.Errorf("getJBODSensors(): %v", err)
			}
		}
	}

	// License
	err = e.getLicenseInfo(ch)
	if err != nil {
		err = fmt.Errorf("getLicenseInfo(): %v", err)
	}

	return err
}

func (e *Exporter) getVolumes(ch chan<- prometheus.Metric) ([]string, error) {

	apiResponse, err := e.queryApi(ch, "volume", "get_names", []string{""}); if err != nil { return nil, err }
	apiResult, err := json.Marshal(apiResponse.Result); if err != nil { return nil, err }

	var volumes []string
	err = json.Unmarshal([]byte(apiResult), &volumes)

	return volumes, err
}

func (e *Exporter) getVolumeStatus(ch chan<- prometheus.Metric, volume string) error {

	apiResponse, err := e.queryApi(ch, "volume", "get_status", []string{volume}); if err != nil { return err }
	apiResult, err := json.Marshal(apiResponse.Result); if err != nil { return err }

	var volumeStatus = new(VolumeStatus)
	err = json.Unmarshal(apiResult, &volumeStatus); if err != nil { return err }

	var volumeOnline float64 = 0
	if len(volumeStatus.State) > 0 {
		if volumeStatus.State[0] == "ONLINE" { volumeOnline = 1 }
	}

	ch <- prometheus.MustNewConstMetric(e.volumeOnline, prometheus.GaugeValue, volumeOnline, volume)

	return err
}

func (e *Exporter) getJBODs(ch chan<- prometheus.Metric) ([]string, error) {

	apiResponse, err := e.queryApi(ch, "jbod", "get_names", []string{""}); if err != nil { return nil, err }
	apiResult, err := json.Marshal(apiResponse.Result); if err != nil { return nil, err }

	var jbods []string
	err = json.Unmarshal([]byte(apiResult), &jbods)

	return jbods, err
}

func (e *Exporter) getJBODSensors(ch chan<- prometheus.Metric, jbod string) error {

	apiResponse, err := e.queryApi(ch, "jbod", "get_sensors", []string{jbod}); if err != nil { return err }
	apiResult, err := json.Marshal(apiResponse.Result); if err != nil { return err }

	var sensors []Sensor
	err = json.Unmarshal([]byte(apiResult), &sensors)

	for _, sensor := range sensors {
		var status float64 = 0
		if sensor.Dev == "jbod" {
			switch sensor.Type {
				case "state":
					if sensor.State == "ok" { status = 1 }
					ch <- prometheus.MustNewConstMetric(e.jbodStatus, prometheus.GaugeValue, status, jbod)
					break
				case "temperature":
					value, _ := strconv.ParseFloat(sensor.Value, 64)
					ch <- prometheus.MustNewConstMetric(e.jbodTemp, prometheus.GaugeValue, value, jbod, sensor.Name, sensor.State)
					break
				case "voltage":
					value, _ := strconv.ParseFloat(sensor.Value, 64)
					ch <- prometheus.MustNewConstMetric(e.jbodVoltage, prometheus.GaugeValue, value, jbod, sensor.Name, sensor.State)
					break
			}
		} else {
			slot := strings.Split(sensor.Dev, ":")
			if sensor.State == "ok" { status = 1 }
			ch <- prometheus.MustNewConstMetric(e.jbodSlotStatus, prometheus.GaugeValue, status, jbod, slot[1], sensor.State)
		}
	}

	return err
}

func (e *Exporter) getLicenseInfo(ch chan<- prometheus.Metric) error {

	apiResponse, err := e.queryApi(ch, "appliance", "get_license_info", []string{}); if err != nil { return err }
	apiResult, err := json.Marshal(apiResponse.Result); if err != nil { return err }

	var licenseInfo LicenseInfo
	err = json.Unmarshal([]byte(apiResult), &licenseInfo)

	daysLeft, _ := strconv.ParseFloat(licenseInfo.DaysLeft, 64)
	ch <- prometheus.MustNewConstMetric(e.licenseDaysLeft, prometheus.GaugeValue, daysLeft)

	return err
}

func (e *Exporter) queryApi(ch chan<- prometheus.Metric, object string, method string, params []string) (*ApiResponse, error) {

	reqObject := &ApiRequest{
		Object: object,
		Method: method,
		Params: params,
	}

	e.apiReachable = 0
	reqJson, _ := json.Marshal(reqObject)
	resp, err := e.client.Post(e.URI, "application/json", bytes.NewBuffer(reqJson))
	if err != nil {
		return nil, fmt.Errorf("Error scraping Nexenta API: %v", err)
	}
	e.apiReachable = 1

	data, err := ioutil.ReadAll(resp.Body)
	resp.Body.Close()
	if resp.StatusCode != 200 {
		if err != nil {
			data = []byte(err.Error())
		}
		e.scrapeFailures.Inc()
		e.scrapeFailures.Collect(ch)
		return nil, fmt.Errorf("Request Error: Status %s %s", resp.Status, data)
	}

	//log.Infof("response: %s", data)

	var apiResponse = new(ApiResponse)
	err = json.Unmarshal(data, &apiResponse); if err != nil { return nil, err }

	if apiResponse.Result == nil {
		e.scrapeFailures.Inc()
		e.scrapeFailures.Collect(ch)
		return nil, fmt.Errorf("API Error: %v", apiResponse.Error.Message)
	}

	return apiResponse, nil
}

func main() {
	flag.Parse()

	if *showVersion {
		fmt.Fprintln(os.Stdout, version.Print("nexenta_exporter"))
		os.Exit(0)
	}

	exporter := NewExporter("http://" + *apiUser + ":" + *apiPass + "@" + *apiHost + ":" + *apiPort + apiPath)
	prometheus.MustRegister(exporter)
	prometheus.MustRegister(version.NewCollector("nexenta_exporter"))

	// disable Go metrics
	prometheus.Unregister(prometheus.NewProcessCollector(os.Getpid(), ""))
	prometheus.Unregister(prometheus.NewGoCollector())

	log.Infoln("Starting nexenta_exporter", version.Info())
	log.Infoln("Build context", version.BuildContext())
	log.Infof("Starting Server: %s", *listenAddr)

	http.Handle(*metricsEndpoint, promhttp.Handler())
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte(`<html>
			 <head><title>Nexenta Exporter</title></head>
			 <body>
			 <h1>Nexenta Exporter</h1>
			 <p><a href='` + *metricsEndpoint + `'>Metrics</a></p>
			 </body>
			 </html>`))
	})
	log.Fatal(http.ListenAndServe(*listenAddr, nil))
}