// Package prommatch provides means of checking whether a prometheus metrics
// contain a specific series.
//
// It tries to give a similar look and feel as time series in PromQL.
// So where in PromQL one would write this:
//
//	request_total{direction="outbound", target_port=~"8\d\d\d"} 30
//
// In prommatch, one can write this:
//
//	portRE := regex.MustCompile(`^8\d\d\d$`)
//	prommatch.NewMatcher("request_total", prommatch.Labels{
//		"direction": prommatch.Equals("outbound"),
//		"target_port": prommatch.Like(portRE),
//	})
package prommatch

import (
	"bytes"
	"fmt"
	"net"
	"net/netip"
	"regexp"

	dto "github.com/prometheus/client_model/go"
	"github.com/prometheus/common/expfmt"
	"github.com/prometheus/common/model"
)

// Expression can match or reject one time series.
type Expression interface {
	matches(sp *model.Sample) bool
}

type funcMatcher func(sp *model.Sample) bool

func (fc funcMatcher) matches(sp *model.Sample) bool {
	return fc(sp)
}

// NewMatcher will match series name (exactly) and all the additional matchers.
func NewMatcher(name string, ms ...Expression) *Matcher {
	return &Matcher{
		expressions: append([]Expression{hasName(name)}, ms...),
	}
}

// Matcher contains a list of expressions, which will be checked against each series.
type Matcher struct {
	expressions []Expression
}

// HasMatchInString will return:
// - true, if the provided metrics have a series which matches all expressions,
// - false, if none of the series matches,
// - error, if the provided string is not valid Prometheus metrics,
func (e *Matcher) HasMatchInString(s string) (bool, error) {
	v, err := extractSamplesVectorFromString(s)
	if err != nil {
		return false, fmt.Errorf("failed to parse input string as vector of samples: %w", err)
	}
	return e.hasMatchInVector(v), nil
}

func (e *Matcher) hasMatchInVector(v model.Vector) bool {
	for _, s := range v {
		if e.sampleMatches(s) {
			return true
		}
	}
	return false
}

func (e *Matcher) sampleMatches(s *model.Sample) bool {
	for _, m := range e.expressions {
		if !m.matches(s) {
			return false
		}
	}
	return true
}

// LabelMatcher can match or reject a label's value.
type LabelMatcher func(string) bool

// Labels is used for selecting series with matching labels.
type Labels map[string]LabelMatcher

// Make sure Labels implement Expression.
var _ Expression = Labels{}

func (l Labels) matches(s *model.Sample) bool {
	for k, m := range l {
		labelValue := s.Metric[model.LabelName(k)]
		if !m(string(labelValue)) {
			return false
		}
	}
	return true
}

// Equals is when you want label value to have an exact value.
func Equals(expected string) LabelMatcher {
	return func(s string) bool {
		return expected == s
	}
}

// Like is when you want label value to match a regular expression.
func Like(re *regexp.Regexp) LabelMatcher {
	return func(s string) bool {
		return re.MatchString(s)
	}
}

// Absent is when you want to match series MISSING a specific label.
func Absent() LabelMatcher {
	return func(s string) bool {
		return s == ""
	}
}

// Any is when you want to select a series which has a certain label, but don't care about the value.
func Any() LabelMatcher {
	return func(s string) bool {
		return s != ""
	}
}

// HasValueLike is used for selecting time series based on value.
func HasValueLike(f func(float64) bool) Expression {
	return funcMatcher(func(sp *model.Sample) bool {
		return f(float64(sp.Value))
	})
}

// HasValueOf is used for selecting time series based on a specific value.
func HasValueOf(f float64) Expression {
	return funcMatcher(func(sp *model.Sample) bool {
		return f == float64(sp.Value)
	})
}

// HasPositiveValue is used to select time series with a positive value.
func HasPositiveValue() Expression {
	return HasValueLike(func(f float64) bool {
		return f > 0
	})
}

// IsAddr is used to check if the value is an IP:port combo, where IP can be
// an IPv4 or an IPv6
func IsAddr() LabelMatcher {
	return func(s string) bool {
		if _, err := netip.ParseAddrPort(s); err != nil {
			return false
		}
		return true
	}
}

// IsIP use used to check if the value is an IPv4 or IPv6
func IsIP() LabelMatcher {
	return func(s string) bool {
		return net.ParseIP(s) != nil
	}
}

func hasName(metricName string) Expression {
	return funcMatcher(func(sp *model.Sample) bool {
		return sp.Metric[model.MetricNameLabel] == model.LabelValue(metricName)
	})
}

func extractSamplesVectorFromString(s string) (model.Vector, error) {
	bb := bytes.NewBufferString(s)

	p := &expfmt.TextParser{}
	metricFamilies, err := p.TextToMetricFamilies(bb)
	if err != nil {
		return nil, fmt.Errorf("failed to parse input as metrics: %w", err)
	}
	var mfs []*dto.MetricFamily
	for _, m := range metricFamilies {
		mfs = append(mfs, m)
	}
	v, err := expfmt.ExtractSamples(&expfmt.DecodeOptions{}, mfs...)
	if err != nil {
		return nil, fmt.Errorf("failed to extract samples from input: %w", err)
	}
	return v, nil
}
