// Copyright (C) 2020, MinIO, Inc.
//
// This code is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License, version 3,
// as published by the Free Software Foundation.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License, version 3,
// along with this program.  If not, see <http://www.gnu.org/licenses/>

package configmaps

import (
	"fmt"
	"reflect"
	"time"

	"github.com/golang-jwt/jwt"
	"gopkg.in/yaml.v2"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

	miniov2 "github.com/minio/operator/pkg/apis/minio.min.io/v2"
	v2 "github.com/minio/operator/pkg/apis/minio.min.io/v2"
)

type globalConfig struct {
	ScrapeInterval     time.Duration `yaml:"scrape_interval"`
	EvaluationInterval time.Duration `yaml:"evaluation_interval"`
}

type staticConfig struct {
	Targets []string `yaml:"targets"`
}

type tlsConfig struct {
	CAFile string `yaml:"ca_file"`
}

// ScrapeConfig contains the scrape configuration for prometheus
type ScrapeConfig struct {
	JobName       string         `yaml:"job_name"`
	BearerToken   string         `yaml:"bearer_token"`
	MetricsPath   string         `yaml:"metrics_path"`
	Scheme        string         `yaml:"scheme"`
	TLSConfig     tlsConfig      `yaml:"tls_config"`
	StaticConfigs []staticConfig `yaml:"static_configs"`
}

// PrometheusConfig contains the prometheus configuration
type PrometheusConfig struct {
	Global        globalConfig   `yaml:"global"`
	ScrapeConfigs []ScrapeConfig `yaml:"scrape_configs"`
}

// ConfigFile returns the prometheus config yaml
func (p *PrometheusConfig) ConfigFile() string {
	d, err := yaml.Marshal(p)
	if err != nil {
		panic(fmt.Sprintf("error marshaling to yaml: %v", err))
	}

	configFileContent := fmt.Sprintf(`# This file and config-map is generated by MinIO Operator.
# DO NOT EDIT.

%s`, d)
	return configFileContent
}

// GetPrometheusConfig returns configuration for Prometheus.
func GetPrometheusConfig(t *miniov2.Tenant, accessKey, secretKey string) *PrometheusConfig {
	bearerToken := t.GenBearerToken(accessKey, secretKey)
	minioTargets := t.MinIOServerHostAddress()
	minioScheme := "http"
	if t.TLS() {
		minioScheme = "https"
	}

	// populate config
	promConfig := &PrometheusConfig{
		Global: globalConfig{
			ScrapeInterval:     v2.MinIOPrometheusScrapeInterval,
			EvaluationInterval: 30 * time.Second,
		},
		ScrapeConfigs: []ScrapeConfig{
			{
				JobName:     t.PrometheusConfigJobName(),
				BearerToken: bearerToken,
				MetricsPath: v2.MinIOPrometheusPathCluster,
				Scheme:      minioScheme,
				TLSConfig: tlsConfig{
					CAFile: "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt",
				},
				StaticConfigs: []staticConfig{
					{
						Targets: []string{minioTargets},
					},
				},
			},
		},
	}
	return promConfig
}

const prometheusYml = "prometheus.yml"

// fromPrometheusConfigMap parses prometheus config file from the given
// configmap and returns *prometheusConfig on success. Otherwise returns error.
func fromPrometheusConfigMap(configMap *corev1.ConfigMap) (*PrometheusConfig, error) {
	configFile := configMap.Data[prometheusYml]
	var config PrometheusConfig
	err := yaml.Unmarshal([]byte(configFile), &config)
	if err != nil {
		return nil, err
	}
	return &config, nil
}

// getConfigMap returns k8s config map for the given tenant
func (p *PrometheusConfig) getConfigMap(tenant *miniov2.Tenant) *corev1.ConfigMap {
	return &corev1.ConfigMap{
		ObjectMeta: metav1.ObjectMeta{
			Name:            tenant.PrometheusConfigMapName(),
			Namespace:       tenant.Namespace,
			OwnerReferences: tenant.OwnerRef(),
		},
		Data: map[string]string{
			prometheusYml: p.ConfigFile(),
		},
	}
}

// bearerTokenNeedsUpdate returns true if the prometheusConfig's bearer token
// can't be verified using the given secretKey
func (p *PrometheusConfig) bearerTokenNeedsUpdate(secretKey string) bool {
	tokenStr := p.ScrapeConfigs[0].BearerToken
	_, err := jwt.Parse(tokenStr, func(token *jwt.Token) (interface{}, error) {
		if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
			return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
		}
		// TODO: add checks on token.Claims like issuer etc.
		return []byte(secretKey), nil
	})
	return err != nil
}

// PrometheusConfigMap returns k8s configmap containing Prometheus configuration.
func PrometheusConfigMap(tenant *miniov2.Tenant, accessKey, secretKey string) *corev1.ConfigMap {
	config := GetPrometheusConfig(tenant, accessKey, secretKey)
	return config.getConfigMap(tenant)
}

// UpdatePrometheusConfigMap checks if the prometheus config map needs update
// and if so returns the updated map. Otherwise it returns nil.
func UpdatePrometheusConfigMap(t *miniov2.Tenant, accessKey, secretKey string, existing *corev1.ConfigMap) *corev1.ConfigMap {
	config := GetPrometheusConfig(t, accessKey, secretKey)
	existingConfig, err := fromPrometheusConfigMap(existing)
	if err != nil {
		// needs update to recover possibly corrupt current config file
		return config.getConfigMap(t)
	}

	// Validate existing config bearer token with the secret key from
	// 'desired' tenant spec. Success indicates that the secret key hasn't
	// changed, so 'mask' the bearer token before performing equality check
	// to determine if the prometheus config requires update.
	needsUpdate := existingConfig.bearerTokenNeedsUpdate(secretKey)
	if !needsUpdate {
		config.ScrapeConfigs[0].BearerToken = ""
		existingConfig.ScrapeConfigs[0].BearerToken = ""
		if !reflect.DeepEqual(existingConfig, config) {
			needsUpdate = true
		}
	}

	if needsUpdate {
		return config.getConfigMap(t)
	}

	return nil
}
