// Copyright 2018 Bloomberg Finance L.P.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package goldpinger

import (
	"context"
	"time"

	"github.com/bloomberg/goldpinger/v3/pkg/models"
	"github.com/go-openapi/strfmt"
	"github.com/prometheus/client_golang/prometheus"
	"go.uber.org/zap"
)

var (
	goldpingerStatsCounter = prometheus.NewCounterVec(
		prometheus.CounterOpts{
			Name: "goldpinger_stats_total",
			Help: "Statistics of calls made in goldpinger instances",
		},
		[]string{
			"goldpinger_instance",
			"group",
			"action",
		},
	)

	goldpingerNodesHealthGauge = prometheus.NewGaugeVec(
		prometheus.GaugeOpts{
			Name: "goldpinger_nodes_health_total",
			Help: "Number of nodes seen as healthy/unhealthy from this instance's POV",
		},
		[]string{
			"goldpinger_instance",
			"status",
		},
	)

	goldpingerClusterHealthGauge = prometheus.NewGaugeVec(
		prometheus.GaugeOpts{
			Name: "goldpinger_cluster_health_total",
			Help: "1 if all check pass, 0 otherwise",
		},
		[]string{
			"goldpinger_instance",
		},
	)

	goldpingerResponseTimePeersHistogram = prometheus.NewHistogramVec(
		prometheus.HistogramOpts{
			Name:    "goldpinger_peers_response_time_s",
			Help:    "Histogram of response times from other hosts, when making peer calls",
			Buckets: []float64{.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10, 30},
		},
		[]string{
			"goldpinger_instance",
			"call_type",
			"host_ip",
			"pod_ip",
		},
	)

	goldpingerResponseTimeKubernetesHistogram = prometheus.NewHistogramVec(
		prometheus.HistogramOpts{
			Name:    "goldpinger_kube_master_response_time_s",
			Help:    "Histogram of response times from kubernetes API server, when listing other instances",
			Buckets: []float64{.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10, 30},
		},
		[]string{
			"goldpinger_instance",
		},
	)

	goldpingerErrorsCounter = prometheus.NewCounterVec(
		prometheus.CounterOpts{
			Name: "goldpinger_errors_total",
			Help: "Statistics of errors per instance",
		},
		[]string{
			"goldpinger_instance",
			"type",
		},
	)
	goldpingerDnsErrorsCounter = prometheus.NewCounterVec(
		prometheus.CounterOpts{
			Name: "goldpinger_dns_errors_total",
			Help: "Statistics of DNS errors per instance",
		},
		[]string{
			"goldpinger_instance",
			"host",
		},
	)
	goldPingerTcpErrorsCounter = prometheus.NewCounterVec(
		prometheus.CounterOpts{
			Name: "goldpinger_tcp_errors_total",
			Help: "Statistics of TCP probe errors per instance",
		},
		[]string{
			"goldpinger_instance",
			"host",
		},
	)
	goldPingerHttpErrorsCounter = prometheus.NewCounterVec(
		prometheus.CounterOpts{
			Name: "goldpinger_http_errors_total",
			Help: "Statistics of HTTP probe errors per instance",
		},
		[]string{
			"goldpinger_instance",
			"host",
		},
	)
	goldpingerPeersLossPct = prometheus.NewGaugeVec(
		prometheus.GaugeOpts{
			Name: "goldpinger_peers_loss_pct",
			Help: "UDP packet loss percentage to peer (0-100)",
		},
		[]string{
			"goldpinger_instance",
			"host_ip",
			"pod_ip",
		},
	)
	goldpingerPeersHopCount = prometheus.NewGaugeVec(
		prometheus.GaugeOpts{
			Name: "goldpinger_peers_hop_count",
			Help: "Estimated network hop count to peer from UDP TTL",
		},
		[]string{
			"goldpinger_instance",
			"host_ip",
			"pod_ip",
		},
	)
	goldpingerPeersUDPRtt = prometheus.NewHistogramVec(
		prometheus.HistogramOpts{
			Name:    "goldpinger_peers_udp_rtt_s",
			Help:    "Histogram of UDP round-trip times to peers in seconds",
			Buckets: []float64{.0001, .00025, .0005, .001, .0025, .005, .01, .025, .05, .1, .25, .5, 1},
		},
		[]string{
			"goldpinger_instance",
			"host_ip",
			"pod_ip",
		},
	)
	goldpingerUDPErrorsCounter = prometheus.NewCounterVec(
		prometheus.CounterOpts{
			Name: "goldpinger_udp_errors_total",
			Help: "Statistics of UDP probe errors per instance",
		},
		[]string{
			"goldpinger_instance",
			"host",
		},
	)
	goldpingerUDPDuplicatesCounter = prometheus.NewCounterVec(
		prometheus.CounterOpts{
			Name: "goldpinger_udp_duplicates_total",
			Help: "Count of duplicate UDP reply packets received",
		},
		[]string{
			"goldpinger_instance",
			"host_ip",
			"pod_ip",
		},
	)
	goldpingerUDPOutOfOrderCounter = prometheus.NewCounterVec(
		prometheus.CounterOpts{
			Name: "goldpinger_udp_out_of_order_total",
			Help: "Count of out-of-order UDP reply packets received",
		},
		[]string{
			"goldpinger_instance",
			"host_ip",
			"pod_ip",
		},
	)
	bootTime = time.Now()
)

func init() {
	prometheus.MustRegister(goldpingerStatsCounter)
	prometheus.MustRegister(goldpingerNodesHealthGauge)
	prometheus.MustRegister(goldpingerClusterHealthGauge)
	prometheus.MustRegister(goldpingerResponseTimePeersHistogram)
	prometheus.MustRegister(goldpingerResponseTimeKubernetesHistogram)
	prometheus.MustRegister(goldpingerErrorsCounter)
	prometheus.MustRegister(goldpingerDnsErrorsCounter)
	prometheus.MustRegister(goldPingerHttpErrorsCounter)
	prometheus.MustRegister(goldPingerTcpErrorsCounter)
	prometheus.MustRegister(goldpingerPeersLossPct)
	prometheus.MustRegister(goldpingerPeersHopCount)
	prometheus.MustRegister(goldpingerPeersUDPRtt)
	prometheus.MustRegister(goldpingerUDPErrorsCounter)
	prometheus.MustRegister(goldpingerUDPDuplicatesCounter)
	prometheus.MustRegister(goldpingerUDPOutOfOrderCounter)
	zap.L().Info("Metrics setup - see /metrics")
}

func GetStats(ctx context.Context) *models.PingResults {
	// GetStats no longer populates the received and made calls - use metrics for that instead
	return &models.PingResults{
		BootTime: strfmt.DateTime(bootTime),
	}
}

// counts various calls received and made
func CountCall(group string, call string) {
	goldpingerStatsCounter.WithLabelValues(
		GoldpingerConfig.Hostname,
		group,
		call,
	).Inc()
}

// counts healthy and unhealthy nodes
func CountHealthyUnhealthyNodes(healthy, unhealthy float64) {
	goldpingerNodesHealthGauge.WithLabelValues(
		GoldpingerConfig.Hostname,
		"healthy",
	).Set(healthy)
	goldpingerNodesHealthGauge.WithLabelValues(
		GoldpingerConfig.Hostname,
		"unhealthy",
	).Set(unhealthy)
}

// SetClusterHealth sets the cluster health gauge to 1 (healthy) or 0 (unhealthy)
func SetClusterHealth(healthy bool) {
	value := 1.0
	if !healthy {
		value = 0
	}
	goldpingerClusterHealthGauge.WithLabelValues(
		GoldpingerConfig.Hostname,
	).Set(value)
}

// counts instances of various errors
func CountError(errorType string) {
	goldpingerErrorsCounter.WithLabelValues(
		GoldpingerConfig.Hostname,
		errorType,
	).Inc()
}

// counts instances of dns errors
func CountDnsError(host string) {
	goldpingerDnsErrorsCounter.WithLabelValues(
		GoldpingerConfig.Hostname,
		host,
	).Inc()
}

// CountTcpError counts instances of tcp errors for prober
func CountTcpError(host string) {
	goldPingerTcpErrorsCounter.WithLabelValues(
		GoldpingerConfig.Hostname,
		host,
	).Inc()
}

// CountHttpError counts instances of tcp errors for prober
func CountHttpError(host string) {
	goldPingerHttpErrorsCounter.WithLabelValues(
		GoldpingerConfig.Hostname,
		host,
	).Inc()
}

// SetPeerLossPct sets the UDP packet loss percentage gauge for a peer
func SetPeerLossPct(hostIP, podIP string, lossPct float64) {
	goldpingerPeersLossPct.WithLabelValues(
		GoldpingerConfig.Hostname,
		hostIP,
		podIP,
	).Set(lossPct)
}

// SetPeerHopCount sets the estimated hop count gauge for a peer
func SetPeerHopCount(hostIP, podIP string, hopCount int32) {
	goldpingerPeersHopCount.WithLabelValues(
		GoldpingerConfig.Hostname,
		hostIP,
		podIP,
	).Set(float64(hopCount))
}

// DeletePeerMetrics removes stale metric labels for a destroyed peer.
// The response-time histogram is observed with two call_type values:
// "ping" (continuous from Pinger) and "check" (from CheckAllPods), so both
// must be pruned. Must be called unconditionally when a peer is removed.
func DeletePeerMetrics(hostIP, podIP string) {
	goldpingerResponseTimePeersHistogram.DeleteLabelValues(GoldpingerConfig.Hostname, "ping", hostIP, podIP)
	goldpingerResponseTimePeersHistogram.DeleteLabelValues(GoldpingerConfig.Hostname, "check", hostIP, podIP)
}

// DeletePeerUDPMetrics removes stale UDP metric labels for a destroyed peer.
// This must be kept in sync with all per-peer UDP metrics to avoid stale
// label sets lingering in /metrics after a pod rolls.
func DeletePeerUDPMetrics(hostIP, podIP string) {
	goldpingerPeersLossPct.DeleteLabelValues(GoldpingerConfig.Hostname, hostIP, podIP)
	goldpingerPeersHopCount.DeleteLabelValues(GoldpingerConfig.Hostname, hostIP, podIP)
	goldpingerPeersUDPRtt.DeleteLabelValues(GoldpingerConfig.Hostname, hostIP, podIP)
	goldpingerUDPDuplicatesCounter.DeleteLabelValues(GoldpingerConfig.Hostname, hostIP, podIP)
	goldpingerUDPOutOfOrderCounter.DeleteLabelValues(GoldpingerConfig.Hostname, hostIP, podIP)
	goldpingerUDPErrorsCounter.DeleteLabelValues(GoldpingerConfig.Hostname, pickPodHostIP(podIP, hostIP))
}

// ObservePeerUDPRtt records a UDP RTT observation in seconds
func ObservePeerUDPRtt(hostIP, podIP string, rttS float64) {
	goldpingerPeersUDPRtt.WithLabelValues(
		GoldpingerConfig.Hostname,
		hostIP,
		podIP,
	).Observe(rttS)
}

// CountUDPError counts instances of UDP probe errors
func CountUDPError(host string) {
	goldpingerUDPErrorsCounter.WithLabelValues(
		GoldpingerConfig.Hostname,
		host,
	).Inc()
}

// CountUDPDuplicates adds to the duplicate packet counter for a peer
func CountUDPDuplicates(hostIP, podIP string, n int) {
	goldpingerUDPDuplicatesCounter.WithLabelValues(
		GoldpingerConfig.Hostname,
		hostIP,
		podIP,
	).Add(float64(n))
}

// CountUDPOutOfOrder adds to the out-of-order packet counter for a peer
func CountUDPOutOfOrder(hostIP, podIP string, n int) {
	goldpingerUDPOutOfOrderCounter.WithLabelValues(
		GoldpingerConfig.Hostname,
		hostIP,
		podIP,
	).Add(float64(n))
}

// returns a timer for easy observing of the durations of calls to kubernetes API
func GetLabeledKubernetesCallsTimer() *prometheus.Timer {
	return prometheus.NewTimer(
		goldpingerResponseTimeKubernetesHistogram.WithLabelValues(
			GoldpingerConfig.Hostname,
		),
	)
}

// returns a timer for easy observing of the duration of calls to peers
func GetLabeledPeersCallsTimer(callType, hostIP, podIP string) *prometheus.Timer {
	return prometheus.NewTimer(
		goldpingerResponseTimePeersHistogram.WithLabelValues(
			GoldpingerConfig.Hostname,
			callType,
			hostIP,
			podIP,
		),
	)
}
