package metrics

import (
	"net/http"
	"strconv"
	"time"

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

const (
	namespace = "metaserver"

	ClusterSyncOperationReload    = "reload"
	ClusterSyncOperationReconcile = "reconcile"

	MySQLOperationQuery       = "query"
	MySQLOperationTransaction = "transaction"

	ResultSuccess = "success"
	ResultFailure = "failure"
)

var (
	HTTPRequestsTotal = prometheus.NewCounterVec(
		prometheus.CounterOpts{
			Namespace: namespace,
			Name:      "http_requests_total",
			Help:      "Total number of HTTP requests handled by route, method, and status code.",
		},
		[]string{"route", "method", "code"},
	)
	HTTPRequestDuration = prometheus.NewHistogramVec(
		prometheus.HistogramOpts{
			Namespace: namespace,
			Name:      "http_request_duration_seconds",
			Help:      "HTTP request duration by route and method.",
			Buckets:   prometheus.DefBuckets,
		},
		[]string{"route", "method"},
	)
	ClusterSyncOperationsTotal = prometheus.NewCounterVec(
		prometheus.CounterOpts{
			Namespace: namespace,
			Name:      "cluster_sync_operations_total",
			Help:      "Total number of cluster cache synchronization operations by operation and result.",
		},
		[]string{"operation", "result"},
	)
	ClusterSyncDuration = prometheus.NewHistogramVec(
		prometheus.HistogramOpts{
			Namespace: namespace,
			Name:      "cluster_sync_duration_seconds",
			Help:      "Cluster cache synchronization duration by operation.",
			Buckets:   prometheus.ExponentialBuckets(0.01, 2, 14),
		},
		[]string{"operation"},
	)
	ClusterFullLoadDuration = prometheus.NewHistogramVec(
		prometheus.HistogramOpts{
			Namespace: namespace,
			Name:      "cluster_full_load_duration_seconds",
			Help:      "Duration of loading all clusters with their specs and statuses from MySQL.",
			Buckets:   prometheus.ExponentialBuckets(0.1, 2, 15),
		},
		[]string{"result"},
	)
	ClusterCacheEntries = prometheus.NewGauge(
		prometheus.GaugeOpts{
			Namespace: namespace,
			Name:      "cluster_cache_entries",
			Help:      "Current number of clusters in the in-memory cache.",
		},
	)
	ClusterEventSnapshotEntries = prometheus.NewGauge(
		prometheus.GaugeOpts{
			Namespace: namespace,
			Name:      "cluster_event_snapshot_entries",
			Help:      "Current number of per-cluster latest Events in memory.",
		},
	)
	ClusterEventPollLastSuccessTimestamp = prometheus.NewGauge(
		prometheus.GaugeOpts{
			Namespace: namespace,
			Name:      "cluster_event_poll_last_success_timestamp_seconds",
			Help:      "Unix timestamp when the last successful cluster Event polling cycle started.",
		},
	)
	ClusterEventPollGap = prometheus.NewHistogram(
		prometheus.HistogramOpts{
			Namespace: namespace,
			Name:      "cluster_event_poll_gap_seconds",
			Help:      "Time since the last successful cluster Event polling cycle, observed on every watcher tick.",
			Buckets:   []float64{0, 1, 2, 3, 5, 10, 20, 30, 60, 120, 300, 600, 900},
		},
	)
	ClusterEventFullScanDuration = prometheus.NewHistogramVec(
		prometheus.HistogramOpts{
			Namespace: namespace,
			Name:      "cluster_event_full_scan_duration_seconds",
			Help:      "Retained cluster Event full-scan duration by result.",
			Buckets:   prometheus.ExponentialBuckets(0.01, 2, 17),
		},
		[]string{"result"},
	)
	ClusterEventFullScanReturnedRows = prometheus.NewHistogramVec(
		prometheus.HistogramOpts{
			Namespace: namespace,
			Name:      "cluster_event_full_scan_returned_rows",
			Help:      "Rows returned by MySQL during a retained cluster Event full scan.",
			Buckets:   prometheus.ExponentialBuckets(1, 2, 21),
		},
		[]string{"result"},
	)
	ClusterEventFullScanPages = prometheus.NewHistogramVec(
		prometheus.HistogramOpts{
			Namespace: namespace,
			Name:      "cluster_event_full_scan_pages",
			Help:      "MySQL query pages completed during a retained cluster Event full scan.",
			Buckets:   []float64{0, 1, 2, 5, 10, 20, 50, 100, 200, 500, 1000},
		},
		[]string{"result"},
	)
	ClusterEventGapReloadsTotal = prometheus.NewCounter(
		prometheus.CounterOpts{
			Namespace: namespace,
			Name:      "cluster_event_gap_reloads_total",
			Help:      "Total number of authoritative cluster reloads triggered by an Event polling or retained-scan gap reaching the Event TTL.",
		},
	)
	ClusterEventChangedClusters = prometheus.NewHistogram(
		prometheus.HistogramOpts{
			Namespace: namespace,
			Name:      "cluster_event_changed_clusters",
			Help:      "Number of changed clusters identified in a successful Event ID window diff.",
			Buckets:   []float64{0, 1, 2, 5, 10, 25, 50, 100, 250, 500, 1000},
		},
	)
	MySQLConfigured = prometheus.NewGauge(
		prometheus.GaugeOpts{
			Namespace: namespace,
			Name:      "mysql_configured",
			Help:      "Whether MySQL-backed cluster metadata is configured for this meta-server instance.",
		},
	)
	MySQLLastOperationSuccess = prometheus.NewGauge(
		prometheus.GaugeOpts{
			Namespace: namespace,
			Name:      "mysql_last_operation_success",
			Help:      "Whether the most recently completed MySQL operation succeeded.",
		},
	)
	MySQLOperationsTotal = prometheus.NewCounterVec(
		prometheus.CounterOpts{
			Namespace: namespace,
			Name:      "mysql_operations_total",
			Help:      "Total number of MySQL operations by operation and result.",
		},
		[]string{"operation", "result"},
	)
	MySQLOperationDuration = prometheus.NewHistogramVec(
		prometheus.HistogramOpts{
			Namespace: namespace,
			Name:      "mysql_operation_duration_seconds",
			Help:      "MySQL operation duration by operation and result.",
			Buckets:   prometheus.ExponentialBuckets(0.001, 2, 18),
		},
		[]string{"operation", "result"},
	)
)

func init() {
	prometheus.MustRegister(
		HTTPRequestsTotal,
		HTTPRequestDuration,
		ClusterSyncOperationsTotal,
		ClusterSyncDuration,
		ClusterFullLoadDuration,
		ClusterCacheEntries,
		ClusterEventSnapshotEntries,
		ClusterEventPollLastSuccessTimestamp,
		ClusterEventPollGap,
		ClusterEventFullScanDuration,
		ClusterEventFullScanReturnedRows,
		ClusterEventFullScanPages,
		ClusterEventGapReloadsTotal,
		ClusterEventChangedClusters,
		MySQLConfigured,
		MySQLLastOperationSuccess,
		MySQLOperationsTotal,
		MySQLOperationDuration,
	)
}

func ObserveMySQLOperation(operation string, startedAt time.Time, err error) {
	result := ResultSuccess
	available := float64(1)
	if err != nil {
		result = ResultFailure
		available = 0
	}

	MySQLLastOperationSuccess.Set(available)
	MySQLOperationsTotal.WithLabelValues(operation, result).Inc()
	MySQLOperationDuration.WithLabelValues(operation, result).
		Observe(time.Since(startedAt).Seconds())
}

func InstrumentHandler(route string, next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		start := time.Now()
		method := normalizeMethod(r.Method)
		recorder := &statusRecorder{
			ResponseWriter: w,
			statusCode:     http.StatusOK,
		}

		next.ServeHTTP(recorder, r)

		HTTPRequestsTotal.WithLabelValues(route, method, strconv.Itoa(recorder.statusCode)).Inc()
		HTTPRequestDuration.WithLabelValues(route, method).Observe(time.Since(start).Seconds())
	})
}

func normalizeMethod(method string) string {
	switch method {
	case http.MethodGet,
		http.MethodHead,
		http.MethodPost,
		http.MethodPut,
		http.MethodPatch,
		http.MethodDelete,
		http.MethodConnect,
		http.MethodOptions,
		http.MethodTrace:
		return method
	default:
		return "OTHER"
	}
}

type statusRecorder struct {
	http.ResponseWriter
	statusCode  int
	wroteHeader bool
}

func (r *statusRecorder) WriteHeader(statusCode int) {
	if r.wroteHeader {
		return
	}
	r.statusCode = statusCode
	r.wroteHeader = true
	r.ResponseWriter.WriteHeader(statusCode)
}

func (r *statusRecorder) Write(body []byte) (int, error) {
	if !r.wroteHeader {
		r.WriteHeader(http.StatusOK)
	}
	return r.ResponseWriter.Write(body)
}
