package prom

import (
	"fmt"
	"io"
	"math"
	"strings"

	"github.com/prometheus/common/model"
)

var (
	_ Metric = (*SingleMetric)(nil)
	_ Metric = (*VecMetric)(nil)

	escaper       = strings.NewReplacer("\\", `\\`, "\n", `\n`)
	quotedEscaper = strings.NewReplacer("\\", `\\`, "\n", `\n`, "\"", `\"`)

	VecMetricElementBucket = "_bucket"
	VecMetricElementCount  = "_count"
	VecMetricElementSum    = "_sum"
)

type Metric interface {
	// TODO: extract to adapt different encoders
	EncodePrometheusFormat(w io.Writer) error
}

type SingleMetric struct {
	Name  string
	Help  string
	Type  string
	Value model.SampleValue
}

func (m *SingleMetric) EncodePrometheusFormat(w io.Writer) error {
	_, err := fmt.Fprintf(w, "# HELP %s %s\n", m.Name, m.Help)
	if err != nil {
		return err
	}
	_, err = fmt.Fprintf(w, "# TYPE %s %s\n", m.Name, m.Type)
	if err != nil {
		return err
	}
	_, err = fmt.Fprintf(w, "%s %s\n", m.Name, m.Value.String())
	return err
}

type VecMetric struct {
	Name    string
	Help    string
	Type    string
	Metrics []*VecMetricElement
}

func (m *VecMetric) EncodePrometheusFormat(w io.Writer) error {
	if _, err := fmt.Fprintf(w, "# HELP %s %s\n", m.Name, m.Help); err != nil {
		return err
	}
	if _, err := fmt.Fprintf(w, "# TYPE %s %s\n", m.Name, m.Type); err != nil {
		return err
	}
	for _, me := range m.Metrics {
		if _, err := io.WriteString(w, m.Name); err != nil {
			return err
		}
		if err := me.EncodePrometheusFormat(w); err != nil {
			return err
		}
		if _, err := io.WriteString(w, "\n"); err != nil {
			return err
		}
	}
	return nil
}

type MetricLabel struct {
	Name  string
	Value string
}

func (label *MetricLabel) EncodePrometheusFormat(w io.Writer) error {
	if _, err := io.WriteString(w, label.Name); err != nil {
		return err
	}
	if _, err := io.WriteString(w, `="`); err != nil {
		return err
	}
	if _, err := writeEscapedString(w, label.Value, true); err != nil {
		return err
	}
	if _, err := io.WriteString(w, `"`); err != nil {
		return err
	}
	return nil
}

type VecMetricElement struct {
	Type   string // NOTE: this field should only be used for histogram(sum/count/bucket), leave it empty for other types
	Labels []MetricLabel
	Value  model.SampleValue
}

func (v *VecMetricElement) EncodePrometheusFormat(w io.Writer) error {
	if _, err := io.WriteString(w, v.Type); err != nil {
		return err
	}

	if _, err := io.WriteString(w, "{"); err != nil {
		return err
	}
	for idx, l := range v.Labels {
		if idx > 0 {
			if _, err := io.WriteString(w, ","); err != nil {
				return err
			}
		}
		if err := l.EncodePrometheusFormat(w); err != nil {
			return err
		}
	}
	if _, err := fmt.Fprintf(w, "} "); err != nil {
		return err
	}
	if _, err := writeFloat(w, v.Value); err != nil {
		return err
	}
	return nil
}

func writeEscapedString(w io.Writer, v string, includeDoubleQuote bool) (int, error) {
	if includeDoubleQuote {
		return quotedEscaper.WriteString(w, v)
	}
	return escaper.WriteString(w, v)
}

func writeFloat(w io.Writer, f model.SampleValue) (int, error) {
	f2 := float64(f)
	switch {
	case f2 == 1:
		return io.WriteString(w, "1")
	case f2 == 0:
		return io.WriteString(w, "0")
	case f2 == -1:
		return io.WriteString(w, "-1")
	case math.IsNaN(f2):
		return io.WriteString(w, "NaN")
	case math.IsInf(f2, +1):
		return io.WriteString(w, "+Inf")
	case math.IsInf(f2, -1):
		return io.WriteString(w, "-Inf")
	default:
		return io.WriteString(w, f.String())
	}
}
