package metrics

import (
	"fmt"
	"net/http"

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

type Metrics struct {
	Endpoint     *EndpointMetrics
	Applications *ApplicationMetrics
	Clients      *ClientMetrics
}

var defaultMetrics *Metrics

// EndpointMetrics stores metrics for registry endpoints
type EndpointMetrics struct {
	requestsTotal  *prometheus.CounterVec
	requestsFailed *prometheus.CounterVec
}

// ApplicationMetrics stores metrics for applications
type ApplicationMetrics struct {
	applicationsTotal        prometheus.Gauge
	imagesWatchedTotal       *prometheus.GaugeVec
	imagesUpdatedTotal       *prometheus.CounterVec
	imagesUpdatedErrorsTotal *prometheus.CounterVec
}

// ClientMetrics stores metrics for K8s and ArgoCD clients
type ClientMetrics struct {
	argoCDRequestsTotal        *prometheus.CounterVec
	argoCDRequestsErrorsTotal  *prometheus.CounterVec
	kubeAPIRequestsTotal       prometheus.Counter
	kubeAPIRequestsErrorsTotal prometheus.Counter
}

// StartMetricsServer starts a new HTTP server for metrics on given port
func StartMetricsServer(port int) chan error {
	errCh := make(chan error)
	go func() {
		sm := http.NewServeMux()
		sm.Handle("/metrics", promhttp.Handler())
		errCh <- http.ListenAndServe(fmt.Sprintf(":%d", port), sm)
	}()
	return errCh
}

// NewEndpointMetrics returns a new endpoint metrics object
func NewEndpointMetrics() *EndpointMetrics {
	metrics := &EndpointMetrics{}

	metrics.requestsTotal = promauto.NewCounterVec(prometheus.CounterOpts{
		Name: "argocd_image_updater_registry_requests_total",
		Help: "The total number of requests to this endpoint",
	}, []string{"registry"})
	metrics.requestsFailed = promauto.NewCounterVec(prometheus.CounterOpts{
		Name: "argocd_image_updater_registry_requests_failed_total",
		Help: "The number of failed requests to this endpoint",
	}, []string{"registry"})

	return metrics
}

// NewApplicationsMetrics returns a new application metrics object
func NewApplicationsMetrics() *ApplicationMetrics {
	metrics := &ApplicationMetrics{}

	metrics.applicationsTotal = promauto.NewGauge(prometheus.GaugeOpts{
		Name: "argocd_image_updater_applications_watched_total",
		Help: "The total number of applications watched by Argo CD Image Updater",
	})

	metrics.imagesWatchedTotal = promauto.NewGaugeVec(prometheus.GaugeOpts{
		Name: "argocd_image_updater_images_watched_total",
		Help: "Number of images watched by Argo CD Image Updater",
	}, []string{"application"})

	metrics.imagesUpdatedTotal = promauto.NewCounterVec(prometheus.CounterOpts{
		Name: "argocd_image_updater_images_updated_total",
		Help: "Number of images updates by Argo CD Image Updater",
	}, []string{"application"})

	metrics.imagesUpdatedErrorsTotal = promauto.NewCounterVec(prometheus.CounterOpts{
		Name: "argocd_image_updater_images_errors_total",
		Help: "Number of errors reported by Argo CD Image Updater",
	}, []string{"application"})

	return metrics
}

// NewClientMetrics returns a new client metrics object
func NewClientMetrics() *ClientMetrics {
	metrics := &ClientMetrics{}

	metrics.argoCDRequestsTotal = promauto.NewCounterVec(prometheus.CounterOpts{
		Name: "argocd_image_updater_argocd_api_requests_total",
		Help: "The total number of Argo CD API requests performed by the Argo CD Image Updater",
	}, []string{"argocd_server"})

	metrics.argoCDRequestsErrorsTotal = promauto.NewCounterVec(prometheus.CounterOpts{
		Name: "argocd_image_updater_argocd_api_errors_total",
		Help: "The total number of Argo CD API requests resulting in error",
	}, []string{"argocd_server"})

	metrics.kubeAPIRequestsTotal = promauto.NewCounter(prometheus.CounterOpts{
		Name: "argocd_image_updater_k8s_api_requests_total",
		Help: "The total number of Argo CD API requests resulting in error",
	})

	metrics.kubeAPIRequestsErrorsTotal = promauto.NewCounter(prometheus.CounterOpts{
		Name: "argocd_image_updater_k8s_api_errors_total",
		Help: "The total number of Argo CD API requests resulting in error",
	})

	return metrics
}

func NewMetrics() *Metrics {
	return &Metrics{
		Endpoint:     NewEndpointMetrics(),
		Applications: NewApplicationsMetrics(),
		Clients:      NewClientMetrics(),
	}
}

// Endpoint returns the global EndpointMetrics object
func Endpoint() *EndpointMetrics {
	if defaultMetrics == nil {
		return nil
	}
	return defaultMetrics.Endpoint
}

// Applications returns the global ApplicationMetrics object
func Applications() *ApplicationMetrics {
	if defaultMetrics == nil {
		return nil
	}
	return defaultMetrics.Applications
}

// Clients returns the global ClientMetrics object
func Clients() *ClientMetrics {
	if defaultMetrics == nil {
		return nil
	}
	return defaultMetrics.Clients
}

// IncreaseRequest increases the request counter of EndpointMetrics object
func (epm *EndpointMetrics) IncreaseRequest(registryURL string, isFailed bool) {
	epm.requestsTotal.WithLabelValues(registryURL).Inc()
	if isFailed {
		epm.requestsFailed.WithLabelValues(registryURL).Inc()
	}
}

// SetNumberOfApplications sets the total number of currently watched applications
func (apm *ApplicationMetrics) SetNumberOfApplications(num int) {
	apm.applicationsTotal.Set(float64(num))
}

// SetNumberOfImagesWatched sets the total number of currently watched images for given application
func (apm *ApplicationMetrics) SetNumberOfImagesWatched(application string, num int) {
	apm.imagesWatchedTotal.WithLabelValues(application).Set(float64(num))
}

// IncreaseImageUpdate increases the number of image updates for given application
func (apm *ApplicationMetrics) IncreaseImageUpdate(application string, by int) {
	apm.imagesUpdatedTotal.WithLabelValues(application).Add(float64(by))
}

// IncreaseUpdateErrors increases the number of errors for given application occurred during update process
func (apm *ApplicationMetrics) IncreaseUpdateErrors(application string, by int) {
	apm.imagesUpdatedErrorsTotal.WithLabelValues(application).Add(float64(by))
}

// IncreaseArgoCDClientRequest increases the number of Argo CD API requests for given server
func (cpm *ClientMetrics) IncreaseArgoCDClientRequest(server string, by int) {
	cpm.argoCDRequestsTotal.WithLabelValues(server).Add(float64(by))
}

// IncreaseArgoCDClientError increases the number of failed Argo CD API requests for given server
func (cpm *ClientMetrics) IncreaseArgoCDClientError(server string, by int) {
	cpm.argoCDRequestsErrorsTotal.WithLabelValues(server).Add(float64(by))
}

// IncreaseK8sClientRequest increases the number of K8s API requests
func (cpm *ClientMetrics) IncreaseK8sClientRequest(by int) {
	cpm.kubeAPIRequestsTotal.Add(float64(by))
}

// IncreaseK8sClientRequest increases the number of failed K8s API requests
func (cpm *ClientMetrics) IncreaseK8sClientError(by int) {
	cpm.kubeAPIRequestsErrorsTotal.Add(float64(by))
}

// InitMetrics initializes the global metrics objects
func InitMetrics() {
	defaultMetrics = NewMetrics()
}
