// Copyright (C) 2020 Emanuele Rocca <ema@wikimedia.org>
// Copyright (C) 2020 Wikimedia Foundation, Inc.
//
// 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 3 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, see <http://www.gnu.org/licenses/>.

package promrdkafka

import (
	"encoding/json"
	"reflect"
	"strings"

	"github.com/prometheus/client_golang/prometheus"
)

type Stats struct {
	Name               string
	Client_id          string
	Type               string
	Ts                 float64
	Time               float64
	Replyq             float64
	Msg_cnt            float64
	Msg_size           float64
	Msg_size_max       float64
	Msg_simple_cnt     float64
	Metadata_cache_cnt float64
	Tx                 float64
	Tx_bytes           float64
	Rx                 float64
	Rx_bytes           float64
	Txmsgs             float64
	Txmsg_bytes        float64
	Rxmsgs             float64
	Rxmsg_bytes        float64
	Brokers            map[string]Broker
	Topics             map[string]Topic
}

type Broker struct {
	Name             string
	Nodeid           float64
	State            string
	Stateage         float64
	Outbuf_cnt       float64
	Outbuf_msg_cnt   float64
	waitresp_cnt     float64
	waitresp_msg_cnt float64
	Tx               float64
	Txbytes          float64
	Txerrs           float64
	Txretries        float64
	Req_timeouts     float64
	Rx               float64
	Rxbytes          float64
	Rxerrs           float64
	Rxcorriderrs     float64
	Rxpartial        float64
	Zbuf_grow        float64
	Buf_grow         float64
	Wakeups          float64
	Int_latency      Percentiles
	Outbuf_latency   Percentiles
	Rtt              Percentiles
	Throttle         Percentiles
}

type Topic struct {
	Topic        string
	Metadata_age float64
	Batchsize    Percentiles
	Batchcnt     Percentiles
	Partitions   map[string]Partition
}

type Percentiles struct {
	Min        float64
	Max        float64
	Avg        float64
	Sum        float64
	Stddev     float64
	P50        float64
	P75        float64
	P90        float64
	P95        float64
	P99        float64
	Outofrange float64
	Hdrsize    float64
	Cnt        float64
}

type Partition struct {
	Leader           float64
	Msgq_cnt         float64
	Msgq_bytes       float64
	Xmit_msgq_cnt    float64
	Xmit_msgq_bytes  float64
	Fetchq_cnt       float64
	Fetchq_size      float64
	Fetch_state      string
	Query_offset     float64
	Next_offset      float64
	App_offset       float64
	Stored_offset    float64
	Committed_offset float64
	Eof_offset       float64
	Lo_offset        float64
	Hi_offset        float64
	Consumer_lag     float64
	Txmsgs           float64
	Txbytes          float64
	Rxmsgs           float64
	Rxbytes          float64
	Msgs             float64
	Rx_ver_drops     float64
}

type Metrics struct {
	values map[string]*prometheus.GaugeVec
}

func NewMetrics() *Metrics {
	m := new(Metrics)
	m.values = map[string]*prometheus.GaugeVec{}
	return m
}

func (m Metrics) setMetric(key string, value float64, labels []string, promLabels prometheus.Labels) {
	if m.values[key] == nil {
		m.values[key] = prometheus.NewGaugeVec(prometheus.GaugeOpts{Name: key}, labels)
		prometheus.Register(m.values[key])
	}

	// Set() on a prometheus.GaugeVec panics if the number of labels passed via
	// With() is higher than the number of elements used when registering the
	// GaugeVec. This can happen right after visiting a structure deeper than
	// our current level.
	pl := prometheus.Labels{}
	for _, val := range labels {
		pl[val] = promLabels[val]
	}
	m.values[key].With(pl).Set(value)
}

func (m Metrics) iterStruct(prefix string, v reflect.Value, labels []string, promLabels prometheus.Labels) {
	st := v.Type()
	for i := 0; i < v.NumField(); i++ {
		f := v.Field(i)
		typ := f.Type()
		key := strings.ToLower(st.Field(i).Name)

		if typ.String() == "string" {
			// Things like "client_id": "atskafka"
			continue
		}

		if typ.String() == "float64" {
			// This is a metric
			m.setMetric(prefix+"_"+key, f.Float(), labels, promLabels)
		} else if strings.HasPrefix(typ.String(), "map") {
			// This is a map, process all values and use the key as label
			iter := f.MapRange()
			for iter.Next() {
				promLabels[key] = iter.Key().String()
				m.iterStruct(prefix+"_"+key, iter.Value(), append(labels, key), promLabels)
			}
		} else {
			// A struct
			m.iterStruct(prefix+"_"+key, f, labels, promLabels)
		}
	}
}

func (m Metrics) Update(kafkaStats string) error {
	var stats Stats

	err := json.Unmarshal([]byte(kafkaStats), &stats)
	if err != nil {
		return err
	}

	s := reflect.ValueOf(&stats).Elem()
	m.iterStruct("rdkafka_"+stats.Type, s, []string{"client_id"},
		prometheus.Labels{"client_id": stats.Client_id})
	return nil
}
