package collector

import (
	"context"
	"fmt"
	"net"
	"time"

	"github.com/go-logr/logr"
	"github.com/go-logr/zapr"
	"github.com/prometheus/client_golang/prometheus"
	"go.uber.org/zap"
	"golang.org/x/sync/errgroup"

	"github.com/cirocosta/go-monero/pkg/rpc/daemon"
)

// CountryMapper defines the signature of a function that given an IP,
// translates it into a country name.
//
//	f(ip) -> CN
//
type CountryMapper func(net.IP) (string, error)

// Collector implements the prometheus Collector interface, providing monero
// metrics whenever a prometheus scrape is received.
//
type Collector struct {
	// client is a Go client that communicated with a `monero` daemon via
	// plain HTTP(S) RPC.
	//
	client *daemon.Client

	// countryMapper is a function that knows how to translate IPs to
	// country codes.
	//
	// optional: if nil, no country-mapping will take place.
	//
	countryMapper CountryMapper

	log logr.Logger
}

// ensure that we implement prometheus' collector interface.
//
var _ prometheus.Collector = &Collector{}

// Option is a type used by functional arguments to mutate the collector to
// override default behavior.
//
type Option func(c *Collector)

// WithCountryMapper is a functional argument that overrides the default no-op
// country mapper.
//
func WithCountryMapper(v CountryMapper) func(c *Collector) {
	return func(c *Collector) {
		c.countryMapper = v
	}
}

func defaultCountryMapper(_ net.IP) (string, error) {
	return "unknown", nil
}

// Register registers this collector with the global prometheus collectors
// registry making it available for an exporter to collect our metrics.
//
func Register(client *daemon.Client, opts ...Option) error {
	defaultLogger, err := zap.NewDevelopment()
	if err != nil {
		return fmt.Errorf("zap new development: %w", err)
	}

	c := &Collector{
		client:        client,
		countryMapper: defaultCountryMapper,
		log:           zapr.NewLogger(defaultLogger),
	}

	for _, opt := range opts {
		opt(c)
	}

	if err := prometheus.Register(c); err != nil {
		return fmt.Errorf("register: %w", err)
	}

	return nil
}

// CollectFunc defines a standardized signature for functions that want to
// expose metrics for collection.
//
type CollectFunc func(ctx context.Context, ch chan<- prometheus.Metric) error

// Describe implements the Describe function of the Collector interface.
//
func (c *Collector) Describe(ch chan<- *prometheus.Desc) {
	// Because we can present the description of the metrics at collection
	// time, we don't need to write anything to the channel.
}

type CustomCollector interface {
	Name() string
	Collect(ctx context.Context) error
}

// Collect implements the Collect function of the Collector interface.
//
// Here is where all of the calls to a monero rpc endpoint is made, each being
// wrapped in its own function, all being called concurrently.
//
func (c *Collector) Collect(ch chan<- prometheus.Metric) {
	var g *errgroup.Group

	ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
	defer cancel()

	g, ctx = errgroup.WithContext(ctx)

	for _, collector := range []CustomCollector{
		NewLastBlockStatsCollector(c.client, ch),
		NewTransactionPoolCollector(c.client, ch),
		NewRPCCollector(c.client, ch),
		NewConnectionsCollector(c.client, ch),
		NewPeersCollector(c.client, ch),
		NewNetStatsCollector(c.client, ch),
		NewOverallCollector(c.client, ch),
	} {
		collector := collector

		g.Go(func() error {
			if err := collector.Collect(ctx); err != nil {
				return fmt.Errorf("%s collect: %w",
					collector.Name(), err)
			}

			return nil
		})
	}

	if err := g.Wait(); err != nil {
		c.log.Error(err, "wait")
	}
}
