package main

import (
	"context"
	"errors"
	"fmt"
	"log/slog"
	"os"
	"time"

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

	"github.com/mjl-/mox/dane"
	"github.com/mjl-/mox/dkim"
	"github.com/mjl-/mox/dmarc"
	"github.com/mjl-/mox/dns"
	"github.com/mjl-/mox/dnsbl"
	"github.com/mjl-/mox/iprev"
	"github.com/mjl-/mox/metrics"
	"github.com/mjl-/mox/mlog"
	"github.com/mjl-/mox/mtasts"
	"github.com/mjl-/mox/smtpclient"
	"github.com/mjl-/mox/spf"
	"github.com/mjl-/mox/subjectpass"
	"github.com/mjl-/mox/tlsrpt"
	"github.com/mjl-/mox/updates"
)

var metricHTTPClient = promauto.NewHistogramVec(
	prometheus.HistogramOpts{
		Name:    "mox_httpclient_request_duration_seconds",
		Help:    "HTTP requests lookups.",
		Buckets: []float64{0.01, 0.05, 0.100, 0.5, 1, 5, 10, 20, 30},
	},
	[]string{
		"pkg",
		"method",
		"code",
		"result",
	},
)

// httpClientObserve tracks the result of an HTTP transaction in a metric, and
// logs the result.
func httpClientObserve(ctx context.Context, elog *slog.Logger, pkg, method string, statusCode int, err error, start time.Time) {
	log := mlog.New("metrics", elog)
	var result string
	switch {
	case err == nil:
		switch statusCode / 100 {
		case 2:
			result = "ok"
		case 4:
			result = "usererror"
		case 5:
			result = "servererror"
		default:
			result = "other"
		}
	case errors.Is(err, os.ErrDeadlineExceeded) || errors.Is(err, context.DeadlineExceeded):
		result = "timeout"
	case errors.Is(err, context.Canceled):
		result = "canceled"
	default:
		result = "error"
	}
	metricHTTPClient.WithLabelValues(pkg, method, result, fmt.Sprintf("%d", statusCode)).Observe(float64(time.Since(start)) / float64(time.Second))
	log.Debugx("httpclient result", err,
		slog.String("pkg", pkg),
		slog.String("method", method),
		slog.Int("code", statusCode),
		slog.Duration("duration", time.Since(start)))
}

func init() {
	dane.MetricVerify = promauto.NewCounter(
		prometheus.CounterOpts{
			Name: "mox_dane_verify_total",
			Help: "Total number of DANE verification attempts, including mox_dane_verify_errors_total.",
		},
	)
	dane.MetricVerifyErrors = promauto.NewCounter(
		prometheus.CounterOpts{
			Name: "mox_dane_verify_errors_total",
			Help: "Total number of DANE verification failures, causing connections to fail.",
		},
	)

	dkim.MetricSign = counterVec{promauto.NewCounterVec(
		prometheus.CounterOpts{
			Name: "mox_dkim_sign_total",
			Help: "DKIM messages signings, label key is the type of key, rsa or ed25519.",
		},
		[]string{
			"key",
		},
	)}
	dkim.MetricVerify = histogramVec{
		promauto.NewHistogramVec(
			prometheus.HistogramOpts{
				Name:    "mox_dkim_verify_duration_seconds",
				Help:    "DKIM verify, including lookup, duration and result.",
				Buckets: []float64{0.001, 0.005, 0.01, 0.05, 0.100, 0.5, 1, 5, 10, 20},
			},
			[]string{
				"algorithm",
				"status",
			},
		),
	}

	dmarc.MetricVerify = histogramVec{promauto.NewHistogramVec(
		prometheus.HistogramOpts{
			Name:    "mox_dmarc_verify_duration_seconds",
			Help:    "DMARC verify, including lookup, duration and result.",
			Buckets: []float64{0.001, 0.005, 0.01, 0.05, 0.100, 0.5, 1, 5, 10, 20},
		},
		[]string{
			"status",
			"reject", // yes/no
			"use",    // yes/no, if policy is used after random selection
		},
	)}
	dns.MetricLookup = histogramVec{
		promauto.NewHistogramVec(
			prometheus.HistogramOpts{
				Name:    "mox_dns_lookup_duration_seconds",
				Help:    "DNS lookups.",
				Buckets: []float64{0.001, 0.005, 0.01, 0.05, 0.100, 0.5, 1, 5, 10, 20, 30},
			},
			[]string{
				"pkg",
				"type",   // Lower-case Resolver method name without leading Lookup.
				"result", // ok, nxdomain, temporary, timeout, canceled, error
			},
		),
	}

	dnsbl.MetricLookup = histogramVec{promauto.NewHistogramVec(
		prometheus.HistogramOpts{
			Name:    "mox_dnsbl_lookup_duration_seconds",
			Help:    "DNSBL lookup",
			Buckets: []float64{0.001, 0.005, 0.01, 0.05, 0.100, 0.5, 1, 5, 10, 20},
		},
		[]string{
			"zone",
			"status",
		},
	)}

	iprev.MetricIPRev = histogramVec{promauto.NewHistogramVec(
		prometheus.HistogramOpts{
			Name:    "mox_iprev_lookup_total",
			Help:    "Number of iprev lookups.",
			Buckets: []float64{0.001, 0.005, 0.01, 0.05, 0.100, 0.5, 1, 5, 10, 20, 30},
		},
		[]string{"status"},
	)}

	mtasts.MetricGet = histogramVec{promauto.NewHistogramVec(
		prometheus.HistogramOpts{
			Name:    "mox_mtasts_get_duration_seconds",
			Help:    "MTA-STS get of policy, including lookup, duration and result.",
			Buckets: []float64{0.01, 0.05, 0.100, 0.5, 1, 5, 10, 20},
		},
		[]string{
			"result", // ok, lookuperror, fetcherror
		},
	)}
	mtasts.HTTPClientObserve = httpClientObserve

	smtpclient.MetricCommands = histogramVec{promauto.NewHistogramVec(
		prometheus.HistogramOpts{
			Name:    "mox_smtpclient_command_duration_seconds",
			Help:    "SMTP client command duration and result codes in seconds.",
			Buckets: []float64{0.001, 0.005, 0.01, 0.05, 0.100, 0.5, 1, 5, 10, 20, 30, 60, 120},
		},
		[]string{
			"cmd",
			"code",
			"secode",
		},
	)}
	smtpclient.MetricTLSRequiredNoIgnored = counterVec{promauto.NewCounterVec(
		prometheus.CounterOpts{
			Name: "mox_smtpclient_tlsrequiredno_ignored_total",
			Help: "Connection attempts with TLS policy findings ignored due to message with TLS-Required: No header. Does not cover case where TLS certificate cannot be PKIX-verified.",
		},
		[]string{
			"ignored", // daneverification (no matching tlsa record)
		},
	)}
	smtpclient.MetricPanicInc = func() {
		metrics.PanicInc(metrics.Smtpclient)
	}

	spf.MetricVerify = histogramVec{promauto.NewHistogramVec(
		prometheus.HistogramOpts{
			Name:    "mox_spf_verify_duration_seconds",
			Help:    "SPF verify, including lookup, duration and result.",
			Buckets: []float64{0.001, 0.005, 0.01, 0.05, 0.100, 0.5, 1, 5, 10, 20},
		},
		[]string{
			"status",
		},
	)}

	subjectpass.MetricGenerate = promauto.NewCounter(
		prometheus.CounterOpts{
			Name: "mox_subjectpass_generate_total",
			Help: "Number of generated subjectpass challenges.",
		},
	)
	subjectpass.MetricVerify = counterVec{promauto.NewCounterVec(
		prometheus.CounterOpts{
			Name: "mox_subjectpass_verify_total",
			Help: "Number of subjectpass verifications.",
		},
		[]string{
			"result", // ok, fail
		},
	)}

	tlsrpt.MetricLookup = histogramVec{promauto.NewHistogramVec(
		prometheus.HistogramOpts{
			Name:    "mox_tlsrpt_lookup_duration_seconds",
			Help:    "TLSRPT lookups with result.",
			Buckets: []float64{0.001, 0.005, 0.01, 0.05, 0.100, 0.5, 1, 5, 10, 20, 30},
		},
		[]string{"result"},
	)}

	updates.MetricLookup = histogramVec{promauto.NewHistogramVec(
		prometheus.HistogramOpts{
			Name:    "mox_updates_lookup_duration_seconds",
			Help:    "Updates lookup with result.",
			Buckets: []float64{0.001, 0.005, 0.01, 0.05, 0.100, 0.5, 1, 5, 10, 20, 30},
		},
		[]string{"result"},
	)}
	updates.MetricFetchChangelog = histogramVec{promauto.NewHistogramVec(
		prometheus.HistogramOpts{
			Name:    "mox_updates_fetchchangelog_duration_seconds",
			Help:    "Fetch changelog with result.",
			Buckets: []float64{0.001, 0.005, 0.01, 0.05, 0.100, 0.5, 1, 5, 10, 20, 30},
		},
		[]string{"result"},
	)}
}

type counterVec struct {
	*prometheus.CounterVec
}

func (m counterVec) IncLabels(labels ...string) {
	m.CounterVec.WithLabelValues(labels...).Inc()
}

type histogramVec struct {
	*prometheus.HistogramVec
}

func (m histogramVec) ObserveLabels(v float64, labels ...string) {
	m.HistogramVec.WithLabelValues(labels...).Observe(v)
}