//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The ASF licenses this file
// to you 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 pf

import (
	"fmt"
	"net/http"
	"time"

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

	log "github.com/apache/pulsar/pulsar-function-go/logutil"
	"github.com/prometheus/client_golang/prometheus"
	prometheus_client "github.com/prometheus/client_model/go"
)

var (
	metricsLabelNames          = []string{"tenant", "namespace", "name", "instance_id", "cluster", "fqfn"}
	exceptionLabelNames        = []string{"error"}
	exceptionMetricsLabelNames = append(metricsLabelNames, exceptionLabelNames...)
	userLabelNames             = []string{"metric"}
	userMetricLabelNames       = append(metricsLabelNames, userLabelNames...)
)

const (
	PulsarFunctionMetricsPrefix = "pulsar_function_"

	TotalSuccessfullyProcessed = "processed_successfully_total"
	TotalSystemExceptions      = "system_exceptions_total"
	TotalUserExceptions        = "user_exceptions_total"
	ProcessLatencyMs           = "process_latency_ms"
	LastInvocation             = "last_invocation"
	TotalReceived              = "received_total"

	TotalSuccessfullyProcessed1min = "processed_successfully_total_1min"
	TotalSystemExceptions1min      = "system_exceptions_total_1min"
	TotalUserExceptions1min        = "user_exceptions_total_1min"
	ProcessLatencyMs1min           = "process_latency_ms_1min"
	TotalReceived1min              = "received_total_1min"

	UserMetric = "user_metric"
)

// Declare Prometheus
var (
	statTotalProcessedSuccessfully = prometheus.NewGaugeVec(
		prometheus.GaugeOpts{
			Name: PulsarFunctionMetricsPrefix + TotalSuccessfullyProcessed,
			Help: "Total number of messages processed successfully."},
		metricsLabelNames)
	statTotalSysExceptions = prometheus.NewGaugeVec(
		prometheus.GaugeOpts{
			Name: PulsarFunctionMetricsPrefix + TotalSystemExceptions,
			Help: "Total number of system exceptions."},
		metricsLabelNames)
	statTotalUserExceptions = prometheus.NewGaugeVec(
		prometheus.GaugeOpts{
			Name: PulsarFunctionMetricsPrefix + TotalUserExceptions,
			Help: "Total number of user exceptions."},
		metricsLabelNames)

	statProcessLatencyMs = prometheus.NewSummaryVec(
		prometheus.SummaryOpts{
			Name: PulsarFunctionMetricsPrefix + ProcessLatencyMs,
			Help: "Process latency in milliseconds."}, metricsLabelNames)

	statLastInvocation = prometheus.NewGaugeVec(
		prometheus.GaugeOpts{
			Name: PulsarFunctionMetricsPrefix + LastInvocation,
			Help: "The timestamp of the last invocation of the function."}, metricsLabelNames)

	statTotalReceived = prometheus.NewGaugeVec(
		prometheus.GaugeOpts{
			Name: PulsarFunctionMetricsPrefix + TotalReceived,
			Help: "Total number of messages received from source."}, metricsLabelNames)

	// 1min windowed metrics
	statTotalProcessedSuccessfully1min = prometheus.NewGaugeVec(
		prometheus.GaugeOpts{
			Name: PulsarFunctionMetricsPrefix + TotalSuccessfullyProcessed1min,
			Help: "Total number of messages processed successfully in the last 1 minute."}, metricsLabelNames)
	statTotalSysExceptions1min = prometheus.NewGaugeVec(
		prometheus.GaugeOpts{
			Name: PulsarFunctionMetricsPrefix + TotalSystemExceptions1min,
			Help: "Total number of system exceptions in the last 1 minute."},
		metricsLabelNames)
	statTotalUserExceptions1min = prometheus.NewGaugeVec(
		prometheus.GaugeOpts{
			Name: PulsarFunctionMetricsPrefix + TotalUserExceptions1min,
			Help: "Total number of user exceptions in the last 1 minute."},
		metricsLabelNames)

	statProcessLatencyMs1min = prometheus.NewSummaryVec(
		prometheus.SummaryOpts{
			Name: PulsarFunctionMetricsPrefix + ProcessLatencyMs1min,
			Help: "Process latency in milliseconds in the last 1 minute."}, metricsLabelNames)

	statTotalReceived1min = prometheus.NewGaugeVec(
		prometheus.GaugeOpts{
			Name: PulsarFunctionMetricsPrefix + TotalReceived1min,
			Help: "Total number of messages received from source in the last 1 minute."}, metricsLabelNames)

	userExceptions = prometheus.NewGaugeVec(
		prometheus.GaugeOpts{
			Name: PulsarFunctionMetricsPrefix + "user_exception",
			Help: "Exception from user code."}, exceptionMetricsLabelNames)

	systemExceptions = prometheus.NewGaugeVec(
		prometheus.GaugeOpts{
			Name: PulsarFunctionMetricsPrefix + "system_exception",
			Help: "Exception from system code."}, exceptionMetricsLabelNames)

	userMetricSummary = prometheus.NewSummaryVec(
		prometheus.SummaryOpts{
			Name: PulsarFunctionMetricsPrefix + UserMetric,
			Help: "User defined metric.",
			Objectives: map[float64]float64{
				0.5:   0.01,
				0.9:   0.01,
				0.99:  0.01,
				0.999: 0.01,
			},
		}, userMetricLabelNames)
)

type MetricsServicer struct {
	goInstance *goInstance
	server     *http.Server
}

var reg *prometheus.Registry

func init() {
	reg = prometheus.NewRegistry()
	reg.MustRegister(statTotalProcessedSuccessfully)
	reg.MustRegister(statTotalSysExceptions)
	reg.MustRegister(statTotalUserExceptions)
	reg.MustRegister(statProcessLatencyMs)
	reg.MustRegister(statLastInvocation)
	reg.MustRegister(statTotalReceived)
	reg.MustRegister(statTotalProcessedSuccessfully1min)
	reg.MustRegister(statTotalSysExceptions1min)
	reg.MustRegister(statTotalUserExceptions1min)
	reg.MustRegister(statProcessLatencyMs1min)
	reg.MustRegister(statTotalReceived1min)
	reg.MustRegister(userExceptions)
	reg.MustRegister(systemExceptions)
	reg.MustRegister(userMetricSummary)

}

type LatestException struct {
	exception error
	timestamp int64
}

// Be sure to use the constructor method: NewStatWithLabelValues
type StatWithLabelValues struct {
	statTotalProcessedSuccessfully     prometheus.Gauge
	statTotalSysExceptions             prometheus.Gauge
	statTotalUserExceptions            prometheus.Gauge
	statProcessLatencyMs               prometheus.Observer
	statLastInvocation                 prometheus.Gauge
	statTotalReceived                  prometheus.Gauge
	statTotalProcessedSuccessfully1min prometheus.Gauge
	statTotalSysExceptions1min         prometheus.Gauge
	statTotalUserExceptions1min        prometheus.Gauge
	statTotalReceived1min              prometheus.Gauge
	latestUserException                []LatestException
	latestSysException                 []LatestException
	processStartTime                   int64
	metricsLabels                      []string
}

func NewStatWithLabelValues(metricsLabels ...string) StatWithLabelValues {
	// as optimization
	var statTotalProcessedSuccessfully = statTotalProcessedSuccessfully.WithLabelValues(metricsLabels...)
	var statTotalSysExceptions = statTotalSysExceptions.WithLabelValues(metricsLabels...)
	var statTotalUserExceptions = statTotalUserExceptions.WithLabelValues(metricsLabels...)
	var statProcessLatencyMs = statProcessLatencyMs.WithLabelValues(metricsLabels...)
	var statLastInvocation = statLastInvocation.WithLabelValues(metricsLabels...)
	var statTotalReceived = statTotalReceived.WithLabelValues(metricsLabels...)
	var statTotalProcessedSuccessfully1min = statTotalProcessedSuccessfully1min.WithLabelValues(metricsLabels...)
	var statTotalSysExceptions1min = statTotalSysExceptions1min.WithLabelValues(metricsLabels...)
	var statTotalUserExceptions1min = statTotalUserExceptions1min.WithLabelValues(metricsLabels...)
	//var _stat_process_latency_ms_1min = stat_process_latency_ms_1min.WithLabelValues(metrics_labels...)
	var statTotalReceived1min = statTotalReceived1min.WithLabelValues(metricsLabels...)

	statObj := StatWithLabelValues{
		statTotalProcessedSuccessfully,
		statTotalSysExceptions,
		statTotalUserExceptions,
		statProcessLatencyMs,
		statLastInvocation,
		statTotalReceived,
		statTotalProcessedSuccessfully1min,
		statTotalSysExceptions1min,
		statTotalUserExceptions1min,
		statTotalReceived1min,
		[]LatestException{},
		[]LatestException{},
		0,
		metricsLabels,
	}
	return statObj
}

func filter(
	ss []*prometheus_client.MetricFamily,
	test func(*prometheus_client.MetricFamily) bool) (ret []*prometheus_client.MetricFamily) {
	for _, s := range ss {
		if test(s) {
			ret = append(ret, s)
		}
	}
	return
}

func getFirstMatch(
	metrics []*prometheus_client.Metric,
	test func(*prometheus_client.LabelPair) bool) *prometheus_client.Metric {
	for _, met := range metrics {
		for _, lbl := range met.Label {
			if test(lbl) {
				return met
			}
		}
	}
	return nil
}

func (stat *StatWithLabelValues) setLastInvocation() {
	now := time.Now()
	stat.statLastInvocation.Set(float64(now.UnixNano()))
}

func (stat *StatWithLabelValues) processTimeStart() {
	now := time.Now()
	stat.processStartTime = now.UnixNano()
}

func (stat *StatWithLabelValues) processTimeEnd() {
	if stat.processStartTime != 0 {
		now := time.Now()
		duration := now.UnixNano() - stat.processStartTime
		stat.statProcessLatencyMs.Observe(float64(duration) / 1e6)
	}
}

func (stat *StatWithLabelValues) incrTotalUserExceptions(err error) {
	stat.statTotalUserExceptions.Inc()
	stat.statTotalUserExceptions1min.Inc()
	stat.addUserException(err)
}

func (stat *StatWithLabelValues) addUserException(err error) {
	now := time.Now()
	ts := now.UnixNano()
	errorTS := LatestException{err, ts}
	stat.latestUserException = append(stat.latestUserException, errorTS)
	if len(stat.latestUserException) > 10 {
		stat.latestUserException = stat.latestUserException[1:]
	}
	// report exception via prometheus
	stat.reportUserExceptionPrometheus(err)
}

// @limits(calls=5, period=60)
func (stat *StatWithLabelValues) reportUserExceptionPrometheus(exception error) {
	errorTS := []string{exception.Error()}
	exceptionMetricLabels := append(stat.metricsLabels, errorTS...)
	userExceptions.WithLabelValues(exceptionMetricLabels...).Set(1.0)
}

func (stat *StatWithLabelValues) incrTotalProcessedSuccessfully() {
	stat.statTotalProcessedSuccessfully.Inc()
	stat.statTotalProcessedSuccessfully1min.Inc()
}

func (stat *StatWithLabelValues) incrTotalSysExceptions(exception error) {
	stat.statTotalSysExceptions.Inc()
	stat.statTotalSysExceptions1min.Inc()
	stat.addSysException(exception)
}

func (stat *StatWithLabelValues) addSysException(exception error) {
	now := time.Now()
	ts := now.UnixNano()
	errorTS := LatestException{exception, ts}
	stat.latestSysException = append(stat.latestSysException, errorTS)
	if len(stat.latestSysException) > 10 {
		stat.latestSysException = stat.latestSysException[1:]
	}
	// report exception via prometheus
	stat.reportSystemExceptionPrometheus(exception)
}

// @limits(calls=5, period=60)
func (stat *StatWithLabelValues) reportSystemExceptionPrometheus(exception error) {
	errorTS := []string{exception.Error()}
	exceptionMetricLabels := append(stat.metricsLabels, errorTS...)
	systemExceptions.WithLabelValues(exceptionMetricLabels...).Set(1.0)
}

func (stat *StatWithLabelValues) incrTotalReceived() {
	stat.statTotalReceived.Inc()
	stat.statTotalReceived1min.Inc()
}

func (stat *StatWithLabelValues) reset() {
	stat.statTotalProcessedSuccessfully1min.Set(0.0)
	stat.statTotalUserExceptions1min.Set(0.0)
	stat.statTotalSysExceptions1min.Set(0.0)
	stat.statTotalReceived1min.Set(0.0)
}

func NewMetricsServicer(goInstance *goInstance) *MetricsServicer {
	serveMux := http.NewServeMux()
	pHandler := promhttp.HandlerFor(
		reg,
		promhttp.HandlerOpts{
			EnableOpenMetrics: true,
		},
	)
	serveMux.Handle("/", pHandler)
	serveMux.Handle("/metrics", pHandler)
	server := &http.Server{
		Addr:    fmt.Sprintf(":%d", goInstance.context.GetMetricsPort()),
		Handler: serveMux,
	}
	return &MetricsServicer{
		goInstance,
		server,
	}
}

func (s *MetricsServicer) serve() {
	go func() {
		// create a listener on metrics port
		log.Infof("Starting metrics server on port %d", s.goInstance.context.GetMetricsPort())
		err := s.server.ListenAndServe()
		switch err {
		case nil, http.ErrServerClosed:
		default:
			log.Fatalf("failed to start metrics server: %v", err)
		}
	}()
}

func (s *MetricsServicer) close() {
	err := s.server.Close()
	if err != nil {
		log.Fatalf("failed to close metrics server: %v", err)
	}
}
