package state

import (
	"bytes"
	"encoding/json"
	"expvar"
	"fmt"

	"github.com/golang/protobuf/proto"
	"github.com/prometheus/client_golang/prometheus"
	dto "github.com/prometheus/client_model/go"
	"github.com/spf13/viper"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
)

// MetricFamily is type alias for proto.Message generated
// from github.com/prometheus/client_model/metrics.proto.
type MetricFamily = dto.MetricFamily

// EncodeMetrics encodes metrics from gatherer into MetricsResponse message,
// if something went wrong returns gRPC Status error (can be returned from service).
func EncodeMetrics(g prometheus.Gatherer) (*MetricsResponse, error) {
	metrics, err := g.Gather()
	if err != nil {
		return nil, status.New(codes.Internal, err.Error()).Err()
	}

	results := make([][]byte, 0, len(metrics))
	for _, mf := range metrics {
		item, err := proto.Marshal(mf)
		if err != nil {
			return nil, status.New(codes.Internal, err.Error()).Err()
		}

		results = append(results, item)
	}

	return &MetricsResponse{Metrics: results}, nil
}

// DecodeMetrics decodes metrics from MetricsResponse to []MetricFamily,
// if something went wrong returns error.
func DecodeMetrics(r *MetricsResponse) ([]*MetricFamily, error) {
	metrics := make([]*dto.MetricFamily, 0, len(r.Metrics))
	for i := range r.Metrics {
		mf := new(MetricFamily)
		if err := proto.Unmarshal(r.Metrics[i], mf); err != nil {
			return nil, err
		}
		metrics = append(metrics, mf)
	}

	return metrics, nil
}

// EncodeConfig encodes viper settings into DumpConfig message,
// if something went wrong returns gRPC Status error (can be returned from service).
func EncodeConfig(v *viper.Viper) (*DumpResponse, error) {
	data, err := json.Marshal(v.AllSettings())
	if err != nil {
		return nil, status.Error(codes.Internal, err.Error())
	}

	return &DumpResponse{Config: data}, nil
}

// EncodeVariables encodes debug variables into DumpVarsResponse message.
// Variables encoded into JSON and stored as slice of bytes.
func EncodeVariables() *DumpVarsResponse {
	buf := new(bytes.Buffer)
	buf.WriteString("{\n")
	first := true

	expvar.Do(func(kv expvar.KeyValue) {
		if !first {
			buf.WriteString(",\n")
		}

		first = false

		_, _ = fmt.Fprintf(buf, "%q: %s", kv.Key, kv.Value)
	})

	buf.WriteString("\n}\n")

	return &DumpVarsResponse{Variables: buf.Bytes()}
}