package probers

import (
	"fmt"
	"net"
	"net/url"
	"slices"
	"strconv"
	"strings"

	"github.com/prometheus/client_golang/prometheus"

	"github.com/letsencrypt/boulder/observer/probers"
	"github.com/letsencrypt/boulder/strictyaml"
)

const (
	notAfterName  = "obs_tls_not_after"
	notBeforeName = "obs_tls_not_before"
	reasonName    = "obs_tls_reason"
)

// TLSConf is exported to receive YAML configuration.
type TLSConf struct {
	Hostname string `yaml:"hostname"`
	RootOrg  string `yaml:"rootOrg"`
	RootCN   string `yaml:"rootCN"`
	Response string `yaml:"response"`
}

// Kind returns a name that uniquely identifies the `Kind` of `Configurer`.
func (c TLSConf) Kind() string {
	return "TLS"
}

// UnmarshalSettings takes YAML as bytes and unmarshals it to the to an TLSConf
// object.
func (c TLSConf) UnmarshalSettings(settings []byte) (probers.Configurer, error) {
	var conf TLSConf
	err := strictyaml.Unmarshal(settings, &conf)
	if err != nil {
		return nil, err
	}

	return conf, nil
}

func (c TLSConf) validateHostname() error {
	hostname := c.Hostname

	if strings.Contains(c.Hostname, ":") {
		host, port, err := net.SplitHostPort(c.Hostname)
		if err != nil {
			return fmt.Errorf("invalid 'hostname', got %q, expected a valid hostport: %s", c.Hostname, err)
		}

		_, err = strconv.Atoi(port)
		if err != nil {
			return fmt.Errorf("invalid 'hostname', got %q, expected a valid hostport: %s", c.Hostname, err)
		}
		hostname = host
	}

	url, err := url.Parse(hostname)
	if err != nil {
		return fmt.Errorf("invalid 'hostname', got %q, expected a valid hostname: %s", c.Hostname, err)
	}

	if url.Scheme != "" {
		return fmt.Errorf("invalid 'hostname', got: %q, should not include scheme", c.Hostname)
	}

	return nil
}

func (c TLSConf) validateResponse() error {
	acceptable := []string{"valid", "expired", "revoked"}
	if slices.Contains(acceptable, strings.ToLower(c.Response)) {
		return nil
	}

	return fmt.Errorf(
		"invalid `response`, got %q. Must be one of %s", c.Response, acceptable)
}

// MakeProber constructs a `TLSProbe` object from the contents of the bound
// `TLSConf` object. If the `TLSConf` cannot be validated, an error appropriate
// for end-user consumption is returned instead.
func (c TLSConf) MakeProber(collectors map[string]prometheus.Collector) (probers.Prober, error) {
	// Validate `hostname`
	err := c.validateHostname()
	if err != nil {
		return nil, err
	}

	// Valid `response`
	err = c.validateResponse()
	if err != nil {
		return nil, err
	}

	// Validate the Prometheus collectors that were passed in
	coll, ok := collectors[notAfterName]
	if !ok {
		return nil, fmt.Errorf("tls prober did not receive collector %q", notAfterName)
	}

	notAfterColl, ok := coll.(*prometheus.GaugeVec)
	if !ok {
		return nil, fmt.Errorf("tls prober received collector %q of wrong type, got: %T, expected *prometheus.GaugeVec", notAfterName, coll)
	}

	coll, ok = collectors[notBeforeName]
	if !ok {
		return nil, fmt.Errorf("tls prober did not receive collector %q", notBeforeName)
	}

	notBeforeColl, ok := coll.(*prometheus.GaugeVec)
	if !ok {
		return nil, fmt.Errorf("tls prober received collector %q of wrong type, got: %T, expected *prometheus.GaugeVec", notBeforeName, coll)
	}

	coll, ok = collectors[reasonName]
	if !ok {
		return nil, fmt.Errorf("tls prober did not receive collector %q", reasonName)
	}

	reasonColl, ok := coll.(*prometheus.CounterVec)
	if !ok {
		return nil, fmt.Errorf("tls prober received collector %q of wrong type, got: %T, expected *prometheus.CounterVec", reasonName, coll)
	}

	return TLSProbe{c.Hostname, c.RootOrg, c.RootCN, strings.ToLower(c.Response), notAfterColl, notBeforeColl, reasonColl}, nil
}

// Instrument constructs any `prometheus.Collector` objects the `TLSProbe` will
// need to report its own metrics. A map is returned containing the constructed
// objects, indexed by the name of the Prometheus metric.  If no objects were
// constructed, nil is returned.
func (c TLSConf) Instrument() map[string]prometheus.Collector {
	notBefore := prometheus.Collector(prometheus.NewGaugeVec(
		prometheus.GaugeOpts{
			Name: notBeforeName,
			Help: "Certificate notBefore value as a Unix timestamp in seconds",
		}, []string{"hostname"},
	))
	notAfter := prometheus.Collector(prometheus.NewGaugeVec(
		prometheus.GaugeOpts{
			Name: notAfterName,
			Help: "Certificate notAfter value as a Unix timestamp in seconds",
		}, []string{"hostname"},
	))
	reason := prometheus.Collector(prometheus.NewCounterVec(
		prometheus.CounterOpts{
			Name: reasonName,
			Help: fmt.Sprintf("Reason for TLS Prober check failure. Can be one of %s", getReasons()),
		}, []string{"hostname", "reason"},
	))
	return map[string]prometheus.Collector{
		notAfterName:  notAfter,
		notBeforeName: notBefore,
		reasonName:    reason,
	}
}

// init is called at runtime and registers `TLSConf`, a `Prober` `Configurer`
// type, as "TLS".
func init() {
	probers.Register(TLSConf{})
}
