//go:build !metrics

package monitoring

import (
	"context"
	"crypto/tls"
	"crypto/x509"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"os"
	"time"

	"zotregistry.dev/zot/v2/pkg/log"
)

const (
	httpTimeout = 1 * time.Minute
)

var errInvalidCACert = errors.New("invalid CA certificate")

// MetricsConfig is used to configure the creation of a Node Exporter http client
// that will connect to a particular zot instance.
type MetricsConfig struct {
	// Address of the zot http server
	Address string

	// CACert is an optional path to a PEM-encoded CA certificate file used to
	// verify the zot server's TLS certificate.  When empty the system cert pool
	// is used.  Set this when the zot server uses a self-signed or private CA.
	CACert string

	// Transport to use for the http client.
	Transport *http.Transport

	// HTTPClient is the client to use.
	HTTPClient *http.Client
}

type MetricsClient struct {
	headers http.Header
	config  MetricsConfig
	log     log.Logger
}

func newHTTPMetricsClient(caCertFile string) (*http.Client, error) {
	var rootCAs *x509.CertPool

	if caCertFile != "" {
		caCertPool, err := x509.SystemCertPool()
		if err != nil || caCertPool == nil {
			caCertPool = x509.NewCertPool()
		}

		caCert, err := os.ReadFile(caCertFile)
		if err != nil {
			return nil, fmt.Errorf("metrics client: failed to read CA cert %s: %w", caCertFile, err)
		}

		// Ensure caCertPool is not nil before appending (defensive against SystemCertPool returning (nil, nil))
		if caCertPool == nil {
			caCertPool = x509.NewCertPool()
		}

		if !caCertPool.AppendCertsFromPEM(caCert) {
			return nil, fmt.Errorf("metrics client: no valid PEM certificate found in %s: %w", caCertFile, errInvalidCACert)
		}

		rootCAs = caCertPool
	}

	transport := http.DefaultTransport.(*http.Transport).Clone() //nolint: forcetypeassert
	transport.TLSClientConfig = &tls.Config{
		RootCAs:    rootCAs,
		MinVersion: tls.VersionTLS12,
	}

	return &http.Client{
		Timeout:   httpTimeout,
		Transport: transport,
	}, nil
}

// NewMetricsClient creates a MetricsClient that can be used to retrieve in memory metrics.
// The new MetricsClient retrieved must be cached and reused by the Node Exporter
// in order to prevent concurrent memory leaks.
func NewMetricsClient(config *MetricsConfig, logger log.Logger) *MetricsClient {
	if config.HTTPClient == nil {
		client, err := newHTTPMetricsClient(config.CACert)
		if err != nil {
			logger.Error().Err(err).Msg("failed to create metrics HTTP client; falling back to TLS12/system-root transport")

			fallbackClient, fallbackErr := newHTTPMetricsClient("")
			if fallbackErr != nil {
				logger.Error().Err(fallbackErr).Msg("failed to create fallback metrics HTTP client; using default transport")
				config.HTTPClient = &http.Client{Timeout: httpTimeout}
			} else {
				config.HTTPClient = fallbackClient
			}
		} else {
			config.HTTPClient = client
		}
	}

	return &MetricsClient{config: *config, headers: make(http.Header), log: logger}
}

func (mc *MetricsClient) GetMetrics() (*MetricsInfo, error) {
	metrics := &MetricsInfo{}
	if _, err := mc.makeGETRequest(mc.config.Address+"/metrics", metrics); err != nil {
		return nil, err
	}

	return metrics, nil
}

func (mc *MetricsClient) makeGETRequest(url string, resultsPtr any) (http.Header, error) {
	req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil)
	if err != nil {
		return nil, fmt.Errorf("metric scraping failed: %w", err)
	}

	resp, err := mc.config.HTTPClient.Do(req)
	if err != nil {
		return nil, fmt.Errorf("metric scraping failed: %w", err)
	}

	defer resp.Body.Close()

	if err := json.NewDecoder(resp.Body).Decode(resultsPtr); err != nil {
		return nil, fmt.Errorf("metric scraping failed: %w", err)
	}

	return resp.Header, nil
}
