// Package metrics provides Prometheus collectors and helpers.
package metrics

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

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

// Recorder describes the metrics operations used across the server.
type Recorder interface {
	IncHTTPInFlight()
	DecHTTPInFlight()
	ObserveHTTPRequest(method, route string, status int, duration time.Duration)
	ObserveOperation(channel, operation, result string, duration time.Duration)
	ObserveReadyz(status string)
	IncUserCreated(kind string)
	IncOrgCreated()
	IncRepoCreated()
	IncFirstGitPushUser()
	ObserveUserActivationDuration(duration time.Duration)
	ObserveRepoActivationDuration(duration time.Duration)
	SetActiveUsersDaily(value float64)
	SetActiveReposDaily(value float64)
	IncGitPush()
	IncGitRead()
	IncSearch()
	IncStarCreated()
	IncReleasePublished()
	IncIssueOpened()
	IncIssueClosed()
	IncIssueReopened()
	IncIssueCommentCreated()
	ObserveIssueCloseDuration(duration time.Duration)
	IncFrontendEvent(event, source, page, platform, location, success, trafficSource string)
}

type noopRecorder struct{}

func (noopRecorder) IncHTTPInFlight()                                       {}
func (noopRecorder) DecHTTPInFlight()                                       {}
func (noopRecorder) ObserveHTTPRequest(string, string, int, time.Duration)  {}
func (noopRecorder) ObserveOperation(string, string, string, time.Duration) {}
func (noopRecorder) ObserveReadyz(string)                                   {}
func (noopRecorder) IncUserCreated(string)                                  {}
func (noopRecorder) IncOrgCreated()                                         {}
func (noopRecorder) IncRepoCreated()                                        {}
func (noopRecorder) IncFirstGitPushUser()                                   {}
func (noopRecorder) ObserveUserActivationDuration(time.Duration)            {}
func (noopRecorder) ObserveRepoActivationDuration(time.Duration)            {}
func (noopRecorder) SetActiveUsersDaily(float64)                            {}
func (noopRecorder) SetActiveReposDaily(float64)                            {}
func (noopRecorder) IncGitPush()                                            {}
func (noopRecorder) IncGitRead()                                            {}
func (noopRecorder) IncSearch()                                             {}
func (noopRecorder) IncStarCreated()                                        {}
func (noopRecorder) IncReleasePublished()                                   {}
func (noopRecorder) IncIssueOpened()                                        {}
func (noopRecorder) IncIssueClosed()                                        {}
func (noopRecorder) IncIssueReopened()                                      {}
func (noopRecorder) IncIssueCommentCreated()                                {}
func (noopRecorder) ObserveIssueCloseDuration(time.Duration)                {}
func (noopRecorder) IncFrontendEvent(string, string, string, string, string, string, string) {
}

// PrometheusMetrics collects application metrics via Prometheus.
type PrometheusMetrics struct {
	HTTPRequestsTotal        *prometheus.CounterVec
	HTTPRequestDuration      *prometheus.HistogramVec
	HTTPRequestsInFlight     prometheus.Gauge
	OperationsTotal          *prometheus.CounterVec
	OperationDuration        *prometheus.HistogramVec
	ReadyzTotal              *prometheus.CounterVec
	UserCreatedTotal         *prometheus.CounterVec
	OrgCreatedTotal          prometheus.Counter
	RepoCreatedTotal         prometheus.Counter
	FirstGitPushUserTotal    prometheus.Counter
	UserActivationDuration   prometheus.Histogram
	RepoActivationDuration   prometheus.Histogram
	ActiveUsersDaily         prometheus.Gauge
	ActiveReposDaily         prometheus.Gauge
	GitPushTotal             prometheus.Counter
	GitReadTotal             prometheus.Counter
	SearchTotal              prometheus.Counter
	StarCreatedTotal         prometheus.Counter
	ReleasePublishedTotal    prometheus.Counter
	IssueOpenedTotal         prometheus.Counter
	IssueClosedTotal         prometheus.Counter
	IssueReopenedTotal       prometheus.Counter
	IssueCommentCreatedTotal prometheus.Counter
	IssueCloseDuration       prometheus.Histogram
	FrontendEventsTotal      *prometheus.CounterVec
}

// NewPrometheusMetrics registers and returns the Prometheus collectors.
func NewPrometheusMetrics(reg prometheus.Registerer) *PrometheusMetrics {
	registerDefaultCollectors(reg)

	m := &PrometheusMetrics{
		HTTPRequestsTotal: prometheus.NewCounterVec(
			prometheus.CounterOpts{
				Namespace: "gh_server",
				Subsystem: "http",
				Name:      "requests_total",
				Help:      "Total number of HTTP requests.",
			},
			[]string{"method", "route", "status"},
		),
		HTTPRequestDuration: prometheus.NewHistogramVec(
			prometheus.HistogramOpts{
				Namespace: "gh_server",
				Subsystem: "http",
				Name:      "request_duration_seconds",
				Help:      "Duration of HTTP requests in seconds.",
				Buckets:   prometheus.DefBuckets,
			},
			[]string{"method", "route", "status"},
		),
		HTTPRequestsInFlight: prometheus.NewGauge(
			prometheus.GaugeOpts{
				Namespace: "gh_server",
				Subsystem: "http",
				Name:      "requests_in_flight",
				Help:      "Current number of in-flight HTTP requests.",
			},
		),
		OperationsTotal: prometheus.NewCounterVec(
			prometheus.CounterOpts{
				Namespace: "gh_server",
				Name:      "operation_total",
				Help:      "Total number of business operations.",
			},
			[]string{"channel", "operation", "result"},
		),
		OperationDuration: prometheus.NewHistogramVec(
			prometheus.HistogramOpts{
				Namespace: "gh_server",
				Name:      "operation_duration_seconds",
				Help:      "Duration of business operations in seconds.",
				Buckets:   prometheus.DefBuckets,
			},
			[]string{"channel", "operation", "result"},
		),
		ReadyzTotal: prometheus.NewCounterVec(
			prometheus.CounterOpts{
				Namespace: "gh_server",
				Subsystem: "readyz",
				Name:      "results_total",
				Help:      "Readiness probe results.",
			},
			[]string{"status"},
		),
		UserCreatedTotal: prometheus.NewCounterVec(
			prometheus.CounterOpts{
				Namespace: "gh_server",
				Subsystem: "business",
				Name:      "user_created_total",
				Help:      "Total number of created user accounts by kind.",
			},
			[]string{"kind"},
		),
		OrgCreatedTotal: prometheus.NewCounter(
			prometheus.CounterOpts{
				Namespace: "gh_server",
				Subsystem: "business",
				Name:      "org_created_total",
				Help:      "Total number of created organization accounts.",
			},
		),
		RepoCreatedTotal: prometheus.NewCounter(
			prometheus.CounterOpts{
				Namespace: "gh_server",
				Subsystem: "business",
				Name:      "repo_created_total",
				Help:      "Total number of created repositories.",
			},
		),
		FirstGitPushUserTotal: prometheus.NewCounter(
			prometheus.CounterOpts{
				Namespace: "gh_server",
				Subsystem: "business",
				Name:      "first_git_push_user_total",
				Help:      "Total number of users who completed their first successful Git push.",
			},
		),
		UserActivationDuration: prometheus.NewHistogram(
			prometheus.HistogramOpts{
				Namespace: "gh_server",
				Subsystem: "business",
				Name:      "user_activation_duration_seconds",
				Help:      "Seconds from user creation to first successful Git push.",
				Buckets:   []float64{60, 300, 900, 1800, 3600, 10800, 21600, 43200, 86400, 604800, 2592000},
			},
		),
		RepoActivationDuration: prometheus.NewHistogram(
			prometheus.HistogramOpts{
				Namespace: "gh_server",
				Subsystem: "business",
				Name:      "repo_activation_duration_seconds",
				Help:      "Seconds from repository creation to first successful Git push.",
				Buckets:   []float64{60, 300, 900, 1800, 3600, 10800, 21600, 43200, 86400, 604800, 2592000},
			},
		),
		ActiveUsersDaily: prometheus.NewGauge(
			prometheus.GaugeOpts{
				Namespace: "gh_server",
				Subsystem: "business",
				Name:      "active_users_daily",
				Help:      "Number of users with tracked business activity today.",
			},
		),
		ActiveReposDaily: prometheus.NewGauge(
			prometheus.GaugeOpts{
				Namespace: "gh_server",
				Subsystem: "business",
				Name:      "active_repos_daily",
				Help:      "Number of repositories with tracked business activity today.",
			},
		),
		GitPushTotal: prometheus.NewCounter(
			prometheus.CounterOpts{
				Namespace: "gh_server",
				Subsystem: "business",
				Name:      "git_push_total",
				Help:      "Total number of successful Git pushes that changed refs.",
			},
		),
		GitReadTotal: prometheus.NewCounter(
			prometheus.CounterOpts{
				Namespace: "gh_server",
				Subsystem: "business",
				Name:      "git_read_total",
				Help:      "Total number of successful Git upload-pack requests.",
			},
		),
		SearchTotal: prometheus.NewCounter(
			prometheus.CounterOpts{
				Namespace: "gh_server",
				Subsystem: "business",
				Name:      "search_total",
				Help:      "Total number of successful search requests.",
			},
		),
		StarCreatedTotal: prometheus.NewCounter(
			prometheus.CounterOpts{
				Namespace: "gh_server",
				Subsystem: "business",
				Name:      "star_created_total",
				Help:      "Total number of newly created repository stars.",
			},
		),
		ReleasePublishedTotal: prometheus.NewCounter(
			prometheus.CounterOpts{
				Namespace: "gh_server",
				Subsystem: "business",
				Name:      "release_published_total",
				Help:      "Total number of non-draft releases published.",
			},
		),
		IssueOpenedTotal: prometheus.NewCounter(
			prometheus.CounterOpts{
				Namespace: "gh_server",
				Subsystem: "business",
				Name:      "issue_opened_total",
				Help:      "Total number of issues opened.",
			},
		),
		IssueClosedTotal: prometheus.NewCounter(
			prometheus.CounterOpts{
				Namespace: "gh_server",
				Subsystem: "business",
				Name:      "issue_closed_total",
				Help:      "Total number of issues closed.",
			},
		),
		IssueReopenedTotal: prometheus.NewCounter(
			prometheus.CounterOpts{
				Namespace: "gh_server",
				Subsystem: "business",
				Name:      "issue_reopened_total",
				Help:      "Total number of issues reopened.",
			},
		),
		IssueCommentCreatedTotal: prometheus.NewCounter(
			prometheus.CounterOpts{
				Namespace: "gh_server",
				Subsystem: "business",
				Name:      "issue_comment_created_total",
				Help:      "Total number of issue comments created.",
			},
		),
		IssueCloseDuration: prometheus.NewHistogram(
			prometheus.HistogramOpts{
				Namespace: "gh_server",
				Subsystem: "business",
				Name:      "issue_close_duration_seconds",
				Help:      "Seconds from issue creation to close.",
				Buckets:   []float64{60, 300, 900, 1800, 3600, 10800, 21600, 43200, 86400, 604800, 2592000},
			},
		),
		FrontendEventsTotal: prometheus.NewCounterVec(
			prometheus.CounterOpts{
				Namespace: "gh_server",
				Subsystem: "frontend",
				Name:      "events_total",
				Help:      "Total number of frontend analytics events received.",
			},
			[]string{"event", "source", "page", "platform", "location", "success", "traffic_source"},
		),
	}

	reg.MustRegister(
		m.HTTPRequestsTotal,
		m.HTTPRequestDuration,
		m.HTTPRequestsInFlight,
		m.OperationsTotal,
		m.OperationDuration,
		m.ReadyzTotal,
		m.UserCreatedTotal,
		m.OrgCreatedTotal,
		m.RepoCreatedTotal,
		m.FirstGitPushUserTotal,
		m.UserActivationDuration,
		m.RepoActivationDuration,
		m.ActiveUsersDaily,
		m.ActiveReposDaily,
		m.GitPushTotal,
		m.GitReadTotal,
		m.SearchTotal,
		m.StarCreatedTotal,
		m.ReleasePublishedTotal,
		m.IssueOpenedTotal,
		m.IssueClosedTotal,
		m.IssueReopenedTotal,
		m.IssueCommentCreatedTotal,
		m.IssueCloseDuration,
		m.FrontendEventsTotal,
	)

	return m
}

// IncHTTPInFlight increments the in-flight HTTP requests gauge.
func (m *PrometheusMetrics) IncHTTPInFlight() {
	m.HTTPRequestsInFlight.Inc()
}

// DecHTTPInFlight decrements the in-flight HTTP requests gauge.
func (m *PrometheusMetrics) DecHTTPInFlight() {
	m.HTTPRequestsInFlight.Dec()
}

// ObserveHTTPRequest records a completed HTTP request.
func (m *PrometheusMetrics) ObserveHTTPRequest(method, route string, status int, duration time.Duration) {
	if method == "" {
		method = "UNKNOWN"
	}
	if route == "" {
		route = "unmatched"
	}
	if status == 0 {
		status = http.StatusOK
	}
	statusLabel := strconv.Itoa(status)
	m.HTTPRequestsTotal.WithLabelValues(method, route, statusLabel).Inc()
	m.HTTPRequestDuration.WithLabelValues(method, route, statusLabel).Observe(duration.Seconds())
}

// ObserveOperation records a completed business operation.
func (m *PrometheusMetrics) ObserveOperation(channel, operation, result string, duration time.Duration) {
	if channel == "" {
		channel = "unknown"
	}
	if operation == "" {
		operation = "unknown"
	}
	if result == "" {
		result = "unknown"
	}
	m.OperationsTotal.WithLabelValues(channel, operation, result).Inc()
	m.OperationDuration.WithLabelValues(channel, operation, result).Observe(duration.Seconds())
}

// ObserveReadyz records a readiness probe result.
func (m *PrometheusMetrics) ObserveReadyz(status string) {
	if status == "" {
		status = "unknown"
	}
	m.ReadyzTotal.WithLabelValues(status).Inc()
}

// IncUserCreated records a created user account.
func (m *PrometheusMetrics) IncUserCreated(kind string) {
	if kind == "" {
		kind = "unknown"
	}
	m.UserCreatedTotal.WithLabelValues(kind).Inc()
}

// IncOrgCreated records a created organization account.
func (m *PrometheusMetrics) IncOrgCreated() {
	m.OrgCreatedTotal.Inc()
}

// IncRepoCreated records a created repository.
func (m *PrometheusMetrics) IncRepoCreated() {
	m.RepoCreatedTotal.Inc()
}

// IncFirstGitPushUser records a user's first successful Git push.
func (m *PrometheusMetrics) IncFirstGitPushUser() {
	m.FirstGitPushUserTotal.Inc()
}

// ObserveUserActivationDuration records user activation latency.
func (m *PrometheusMetrics) ObserveUserActivationDuration(duration time.Duration) {
	if duration >= 0 {
		m.UserActivationDuration.Observe(duration.Seconds())
	}
}

// ObserveRepoActivationDuration records repository activation latency.
func (m *PrometheusMetrics) ObserveRepoActivationDuration(duration time.Duration) {
	if duration >= 0 {
		m.RepoActivationDuration.Observe(duration.Seconds())
	}
}

// SetActiveUsersDaily sets today's active user gauge.
func (m *PrometheusMetrics) SetActiveUsersDaily(value float64) {
	m.ActiveUsersDaily.Set(value)
}

// SetActiveReposDaily sets today's active repository gauge.
func (m *PrometheusMetrics) SetActiveReposDaily(value float64) {
	m.ActiveReposDaily.Set(value)
}

// IncGitPush records a successful Git push that changed refs.
func (m *PrometheusMetrics) IncGitPush() {
	m.GitPushTotal.Inc()
}

// IncGitRead records a successful upload-pack request.
func (m *PrometheusMetrics) IncGitRead() {
	m.GitReadTotal.Inc()
}

// IncSearch records a successful search request.
func (m *PrometheusMetrics) IncSearch() {
	m.SearchTotal.Inc()
}

// IncStarCreated records a newly created star.
func (m *PrometheusMetrics) IncStarCreated() {
	m.StarCreatedTotal.Inc()
}

// IncReleasePublished records a non-draft release publication.
func (m *PrometheusMetrics) IncReleasePublished() {
	m.ReleasePublishedTotal.Inc()
}

// IncIssueOpened records an opened issue.
func (m *PrometheusMetrics) IncIssueOpened() {
	m.IssueOpenedTotal.Inc()
}

// IncIssueClosed records a closed issue.
func (m *PrometheusMetrics) IncIssueClosed() {
	m.IssueClosedTotal.Inc()
}

// IncIssueReopened records a reopened issue.
func (m *PrometheusMetrics) IncIssueReopened() {
	m.IssueReopenedTotal.Inc()
}

// IncIssueCommentCreated records a created issue comment.
func (m *PrometheusMetrics) IncIssueCommentCreated() {
	m.IssueCommentCreatedTotal.Inc()
}

// ObserveIssueCloseDuration records issue close latency.
func (m *PrometheusMetrics) ObserveIssueCloseDuration(duration time.Duration) {
	if duration >= 0 {
		m.IssueCloseDuration.Observe(duration.Seconds())
	}
}

// IncFrontendEvent records a normalized frontend analytics event.
func (m *PrometheusMetrics) IncFrontendEvent(event, source, page, platform, location, success, trafficSource string) {
	m.FrontendEventsTotal.WithLabelValues(
		unknownIfEmpty(event),
		unknownIfEmpty(source),
		unknownIfEmpty(page),
		unknownIfEmpty(platform),
		unknownIfEmpty(location),
		unknownIfEmpty(success),
		unknownIfEmpty(trafficSource),
	).Inc()
}

var (
	defaultRecorder   Recorder = noopRecorder{}
	defaultPrometheus *PrometheusMetrics
	defaultHandler    http.Handler
	initOnce          sync.Once
)

// Init registers metrics with the default registry and returns the handler.
func Init() http.Handler {
	initOnce.Do(func() {
		defaultPrometheus = NewPrometheusMetrics(prometheus.DefaultRegisterer)
		defaultRecorder = defaultPrometheus
		defaultHandler = promhttp.Handler()
	})
	return defaultHandler
}

// DefaultPrometheus returns the default Prometheus collector set, if initialized.
func DefaultPrometheus() *PrometheusMetrics {
	return defaultPrometheus
}

// IncHTTPInFlight increments the in-flight HTTP requests gauge.
func IncHTTPInFlight() {
	defaultRecorder.IncHTTPInFlight()
}

// DecHTTPInFlight decrements the in-flight HTTP requests gauge.
func DecHTTPInFlight() {
	defaultRecorder.DecHTTPInFlight()
}

// ObserveHTTPRequest records a completed HTTP request.
func ObserveHTTPRequest(method, route string, status int, duration time.Duration) {
	defaultRecorder.ObserveHTTPRequest(method, route, status, duration)
}

// ObserveOperation records a completed business operation.
func ObserveOperation(channel, operation, result string, duration time.Duration) {
	defaultRecorder.ObserveOperation(channel, operation, result, duration)
}

// ObserveReadyz records readiness probe results.
func ObserveReadyz(status string) {
	defaultRecorder.ObserveReadyz(status)
}

// IncUserCreated records a created user account.
func IncUserCreated(kind string) {
	defaultRecorder.IncUserCreated(kind)
}

// IncOrgCreated records a created organization account.
func IncOrgCreated() {
	defaultRecorder.IncOrgCreated()
}

// IncRepoCreated records a created repository.
func IncRepoCreated() {
	defaultRecorder.IncRepoCreated()
}

// IncFirstGitPushUser records a user's first successful Git push.
func IncFirstGitPushUser() {
	defaultRecorder.IncFirstGitPushUser()
}

// ObserveUserActivationDuration records user activation latency.
func ObserveUserActivationDuration(duration time.Duration) {
	defaultRecorder.ObserveUserActivationDuration(duration)
}

// ObserveRepoActivationDuration records repository activation latency.
func ObserveRepoActivationDuration(duration time.Duration) {
	defaultRecorder.ObserveRepoActivationDuration(duration)
}

// SetActiveUsersDaily sets today's active user gauge.
func SetActiveUsersDaily(value float64) {
	defaultRecorder.SetActiveUsersDaily(value)
}

// SetActiveReposDaily sets today's active repository gauge.
func SetActiveReposDaily(value float64) {
	defaultRecorder.SetActiveReposDaily(value)
}

// IncGitPush records a successful Git push that changed refs.
func IncGitPush() {
	defaultRecorder.IncGitPush()
}

// IncGitRead records a successful upload-pack request.
func IncGitRead() {
	defaultRecorder.IncGitRead()
}

// IncSearch records a successful search request.
func IncSearch() {
	defaultRecorder.IncSearch()
}

// IncStarCreated records a newly created star.
func IncStarCreated() {
	defaultRecorder.IncStarCreated()
}

// IncReleasePublished records a non-draft release publication.
func IncReleasePublished() {
	defaultRecorder.IncReleasePublished()
}

// IncIssueOpened records an opened issue.
func IncIssueOpened() {
	defaultRecorder.IncIssueOpened()
}

// IncIssueClosed records a closed issue.
func IncIssueClosed() {
	defaultRecorder.IncIssueClosed()
}

// IncIssueReopened records a reopened issue.
func IncIssueReopened() {
	defaultRecorder.IncIssueReopened()
}

// IncIssueCommentCreated records a created issue comment.
func IncIssueCommentCreated() {
	defaultRecorder.IncIssueCommentCreated()
}

// ObserveIssueCloseDuration records issue close latency.
func ObserveIssueCloseDuration(duration time.Duration) {
	defaultRecorder.ObserveIssueCloseDuration(duration)
}

// IncFrontendEvent records a normalized frontend analytics event.
func IncFrontendEvent(event, source, page, platform, location, success, trafficSource string) {
	defaultRecorder.IncFrontendEvent(event, source, page, platform, location, success, trafficSource)
}

func unknownIfEmpty(value string) string {
	if value == "" {
		return "unknown"
	}
	return value
}

func registerDefaultCollectors(reg prometheus.Registerer) {
	registerIfAbsent(reg, prometheus.NewGoCollector())
	registerIfAbsent(reg, prometheus.NewProcessCollector(prometheus.ProcessCollectorOpts{}))
}

func registerIfAbsent(reg prometheus.Registerer, collector prometheus.Collector) {
	if err := reg.Register(collector); err != nil {
		if _, ok := err.(prometheus.AlreadyRegisteredError); ok {
			return
		}
		panic(err)
	}
}
