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"
)

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 ResultObject struct {
	State  []string `json:"state"`
	Errors []string `json:"errors"`
}

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

	apiReachable float64

	up             *prometheus.Desc
	scrapeFailures  prometheus.Counter
	volumeOnline   *prometheus.Desc
}

func NewExporter(uri string) *Exporter {
	return &Exporter{
		URI: uri,
		up: prometheus.NewDesc(
			prometheus.BuildFQName(namespace, "", "up"),
			"Is the Nexenta API reachable",
			nil,
			nil),
		scrapeFailures: prometheus.NewCounter(prometheus.CounterOpts{
			Namespace: namespace,
			Name:      "exporter_scrape_failures_total",
			Help:      "Number of errors while scraping the Nexenta API",
		}),
		volumeOnline: prometheus.NewDesc(
			prometheus.BuildFQName(namespace, "volume", "online"),
			"Status of volume.",
			[]string{"volume"},
			nil,
		),
		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
}

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, 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)
			}
		}
	}

	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 result = new(ResultObject)
	err = json.Unmarshal(apiResult, &result); if err != nil { return err }

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

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

	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))
}