package platform

import (
	"context"
	"fmt"
	"net/http"
	"os"
	"sync"

	"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"

	"github.com/go-logr/logr"
	"github.com/icholy/digest"
	"github.com/prometheus/client_golang/prometheus"
)

type Context struct {
	context.Context

	Metrics *metrics.PlatformProberMetrics
	Logger  logr.Logger
	Cluster *GetClusterResp
	Client  *client
}

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

// Prober probes a cluster by querying the platform.
type Prober struct {
	cfg     *config.PlatformProberConfig
	metrics *metrics.PlatformProberMetrics

	logger    logr.Logger
	probers   []PlatformProber
	transport http.RoundTripper
}

func New(cfg *config.PlatformProberConfig, deps *prober.Dependency) (*Prober, error) {
	key, err := os.ReadFile(cfg.PrivateKeyFile)
	if err != nil {
		return nil, fmt.Errorf("unable to read key: %w", err)
	}
	pubKey, err := os.ReadFile(cfg.PublicKeyFile)
	if err != nil {
		return nil, fmt.Errorf("unable to read key: %w", err)
	}

	probers := []PlatformProber{}
	transport := &digest.Transport{
		Username:  string(pubKey),
		Password:  string(key),
		Transport: http.DefaultTransport,
	}

	return &Prober{
		cfg:       cfg,
		metrics:   metrics.NewPlatformProberMetrics(cfg.ClusterID, cfg.ProjectID),
		logger:    deps.Logger.WithName("PlatformProber"),
		probers:   probers,
		transport: transport,
	}, 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 platform")

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

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

func (p *Prober) probe(ctx context.Context, ch chan<- prometheus.Metric) {
	var (
		err error
		pid = p.cfg.ProjectID
		cid = p.cfg.ClusterID
	)

	pctx := &Context{
		Context: ctx,
		Logger:  p.logger.WithValues("project", pid, "cluster", cid),
		Client: &client{
			addr: p.cfg.PlatformAddr,
			httClient: &http.Client{
				Transport: p.transport,
			},
		},
	}

	// probe cluster
	resp, err := pctx.Client.GetCluster(ctx, pid, cid)
	if err != nil {
		if ErrNotFound(err) {
			ch <- p.metrics.ClusterExist(false)
		}
		pctx.Logger.Error(err, "failed to get cluster")
		return
	}
	ch <- p.metrics.ClusterExist(true)
	pctx.Cluster = resp

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