package infra

import (
	"context"
	"sync"

	"github.com/go-logr/logr"
	"github.com/prometheus/client_golang/prometheus"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	"sigs.k8s.io/controller-runtime/pkg/client"

	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-provider/cluster-prober/config"
	"github.com/tidbcloud/infra-provider/cluster-prober/prober"
	"github.com/tidbcloud/infra-provider/cluster-prober/prober/metrics"
)

type Context struct {
	context.Context

	Metrics *metrics.InfraProberMetrics
	Logger  logr.Logger
	Cluster *cloudv1.Cluster
	Client  client.Reader // client for the infra apiserver
}

type InfraProber interface {
	Name() string
	Probe(ctx *Context, ch chan<- prometheus.Metric) error
}

// Prober probes a cluster by querying the infra apiserver.
type Prober struct {
	cfg     *config.InfraProberConfig
	metrics *metrics.InfraProberMetrics
	cli     client.Reader
	logger  logr.Logger
	probers []InfraProber
}

func New(cfg *config.InfraProberConfig, deps *prober.Dependency) (*Prober, error) {
	probers := []InfraProber{
		&clusterStatusProber{},
	}

	return &Prober{
		cfg:     cfg,
		metrics: metrics.NewInfraProberMetrics(cfg.ClusterID, cfg.ClusterNamespace, cfg.ClusterName),
		cli:     deps.InfraClient,
		logger:  deps.Logger.WithName("InfraProber"),
		probers: probers,
	}, nil
}

// Describe implements prometheus.Collector.
func (p *Prober) Describe(ch chan<- *prometheus.Desc) {
	for _, desc := range p.metrics.Desc() {
		ch <- desc
	}
}

// Collect implements prometheus.Collector.
func (p *Prober) Collect(ch chan<- prometheus.Metric) {
	p.logger.V(4).Info("start probe a cluster over infra")

	p.probe(context.Background(), ch)

	p.logger.V(4).Info("probe the cluster over infra done")
}

func (p *Prober) probe(ctx context.Context, ch chan<- prometheus.Metric) {
	var (
		err  error
		ns   = p.cfg.ClusterNamespace
		name = p.cfg.ClusterName
	)
	pctx := &Context{
		Metrics: p.metrics,
		Context: ctx,
		Logger:  p.logger.WithValues("name", name, "namespace", ns),
		Client:  p.cli,
	}

	// probe cluster
	cluster := &cloudv1.Cluster{}
	err = p.cli.Get(ctx, client.ObjectKey{Namespace: ns, Name: name}, cluster)
	if err != nil {
		if apierrors.IsNotFound(err) {
			ch <- p.metrics.ClusterExist(false)
		}
		p.logger.Error(err, "failed to get cluster", "name", name, "namespace", ns)
		return
	}
	ch <- p.metrics.ClusterExist(true)
	pctx.Cluster = cluster

	// other probes
	p.logger.V(4).Info("probe the cluster over infra with other probers")
	wg := sync.WaitGroup{}
	for _, prober := range p.probers {
		wg.Add(1)
		go func(prober InfraProber) {
			defer wg.Done()
			err := prober.Probe(pctx, ch)
			if err != nil {
				p.logger.Error(err, "probe failed", "prober", prober.Name())
			}
		}(prober)
	}
	wg.Wait()
}
