package config

import (
	"errors"
	"io/ioutil"
	"log"
	"os"
	"time"

	"github.com/TwinProduction/gatus/alerting"
	"github.com/TwinProduction/gatus/alerting/alert"
	"github.com/TwinProduction/gatus/alerting/provider"
	"github.com/TwinProduction/gatus/core"
	"github.com/TwinProduction/gatus/k8s"
	"github.com/TwinProduction/gatus/security"
	"github.com/TwinProduction/gatus/storage"
	"github.com/TwinProduction/gatus/util"
	"gopkg.in/yaml.v2"
)

const (
	// DefaultConfigurationFilePath is the default path that will be used to search for the configuration file
	// if a custom path isn't configured through the GATUS_CONFIG_FILE environment variable
	DefaultConfigurationFilePath = "config/config.yaml"

	// DefaultFallbackConfigurationFilePath is the default fallback path that will be used to search for the
	// configuration file if DefaultConfigurationFilePath didn't work
	DefaultFallbackConfigurationFilePath = "config/config.yml"

	// DefaultAddress is the default address the service will bind to
	DefaultAddress = "0.0.0.0"

	// DefaultPort is the default port the service will listen on
	DefaultPort = 8080
)

var (
	// ErrNoServiceInConfig is an error returned when a configuration file has no services configured
	ErrNoServiceInConfig = errors.New("configuration file should contain at least 1 service")

	// ErrConfigFileNotFound is an error returned when the configuration file could not be found
	ErrConfigFileNotFound = errors.New("configuration file not found")

	// ErrInvalidSecurityConfig is an error returned when the security configuration is invalid
	ErrInvalidSecurityConfig = errors.New("invalid security configuration")
)

// Config is the main configuration structure
type Config struct {
	// Debug Whether to enable debug logs
	Debug bool `yaml:"debug"`

	// Metrics Whether to expose metrics at /metrics
	Metrics bool `yaml:"metrics"`

	// SkipInvalidConfigUpdate Whether to make the application ignore invalid configuration
	// if the configuration file is updated while the application is running
	SkipInvalidConfigUpdate bool `yaml:"skip-invalid-config-update"`

	// DisableMonitoringLock Whether to disable the monitoring lock
	// The monitoring lock is what prevents multiple services from being processed at the same time.
	// Disabling this may lead to inaccurate response times
	DisableMonitoringLock bool `yaml:"disable-monitoring-lock"`

	// Security Configuration for securing access to Gatus
	Security *security.Config `yaml:"security"`

	// Alerting Configuration for alerting
	Alerting *alerting.Config `yaml:"alerting"`

	// Services List of services to monitor
	Services []*core.Service `yaml:"services"`

	// Kubernetes is the Kubernetes configuration
	Kubernetes *k8s.Config `yaml:"kubernetes"`

	// Storage is the configuration for how the data is stored
	Storage *storage.Config `yaml:"storage"`

	// Web is the configuration for the web listener
	Web *WebConfig `yaml:"web"`

	filePath        string    // path to the file from which config was loaded from
	lastFileModTime time.Time // last modification time
}

// HasLoadedConfigurationFileBeenModified returns whether the file that the
// configuration has been loaded from has been modified since it was last read
func (config Config) HasLoadedConfigurationFileBeenModified() bool {
	if fileInfo, err := os.Stat(config.filePath); err == nil {
		if !fileInfo.ModTime().IsZero() {
			return config.lastFileModTime.Unix() != fileInfo.ModTime().Unix()
		}
	}
	return false
}

// UpdateLastFileModTime refreshes Config.lastFileModTime
func (config *Config) UpdateLastFileModTime() {
	if fileInfo, err := os.Stat(config.filePath); err == nil {
		if !fileInfo.ModTime().IsZero() {
			config.lastFileModTime = fileInfo.ModTime()
		}
	}
}

// Load loads a custom configuration file
// Note that the misconfiguration of some fields may lead to panics. This is on purpose.
func Load(configFile string) (*Config, error) {
	log.Printf("[config][Load] Reading configuration from configFile=%s", configFile)
	cfg, err := readConfigurationFile(configFile)
	if err != nil {
		if os.IsNotExist(err) {
			return nil, ErrConfigFileNotFound
		}
		return nil, err
	}
	cfg.filePath = configFile
	cfg.UpdateLastFileModTime()
	return cfg, nil
}

// LoadDefaultConfiguration loads the default configuration file
func LoadDefaultConfiguration() (*Config, error) {
	cfg, err := Load(DefaultConfigurationFilePath)
	if err != nil {
		if err == ErrConfigFileNotFound {
			return Load(DefaultFallbackConfigurationFilePath)
		}
		return nil, err
	}
	return cfg, nil
}

func readConfigurationFile(fileName string) (config *Config, err error) {
	var bytes []byte
	if bytes, err = ioutil.ReadFile(fileName); err == nil {
		// file exists, so we'll parse it and return it
		return parseAndValidateConfigBytes(bytes)
	}
	return
}

func parseAndValidateConfigBytes(yamlBytes []byte) (config *Config, err error) {
	// Expand environment variables
	yamlBytes = []byte(os.ExpandEnv(string(yamlBytes)))
	// Parse configuration file
	err = yaml.Unmarshal(yamlBytes, &config)
	if err != nil {
		return
	}
	// Check if the configuration file at least has services configured or Kubernetes auto discovery enabled
	if config == nil || ((config.Services == nil || len(config.Services) == 0) && (config.Kubernetes == nil || !config.Kubernetes.AutoDiscover)) {
		err = ErrNoServiceInConfig
	} else {
		// Note that the functions below may panic, and this is on purpose to prevent Gatus from starting with
		// invalid configurations
		validateAlertingConfig(config.Alerting, config.Services, config.Debug)
		if err := validateSecurityConfig(config); err != nil {
			return nil, err
		}
		if err := validateServicesConfig(config); err != nil {
			return nil, err
		}
		if err := validateKubernetesConfig(config); err != nil {
			return nil, err
		}
		if err := validateWebConfig(config); err != nil {
			return nil, err
		}
		if err := validateStorageConfig(config); err != nil {
			return nil, err
		}
	}
	return
}

func validateStorageConfig(config *Config) error {
	if config.Storage == nil {
		config.Storage = &storage.Config{}
	}
	err := storage.Initialize(config.Storage)
	if err != nil {
		return err
	}
	// Remove all ServiceStatus that represent services which no longer exist in the configuration
	var keys []string
	for _, service := range config.Services {
		keys = append(keys, util.ConvertGroupAndServiceToKey(service.Group, service.Name))
	}
	numberOfServiceStatusesDeleted := storage.Get().DeleteAllServiceStatusesNotInKeys(keys)
	if numberOfServiceStatusesDeleted > 0 {
		log.Printf("[config][validateStorageConfig] Deleted %d service statuses because their matching services no longer existed", numberOfServiceStatusesDeleted)
	}
	return nil
}

func validateWebConfig(config *Config) error {
	if config.Web == nil {
		config.Web = &WebConfig{Address: DefaultAddress, Port: DefaultPort}
	} else {
		return config.Web.validateAndSetDefaults()
	}
	return nil
}

// deprecated
// I don't like the current implementation.
func validateKubernetesConfig(config *Config) error {
	if config.Kubernetes != nil && config.Kubernetes.AutoDiscover {
		if config.Kubernetes.ServiceTemplate == nil {
			return errors.New("kubernetes.service-template cannot be nil")
		}
		if config.Debug {
			log.Println("[config][validateKubernetesConfig] Automatically discovering Kubernetes services...")
		}
		discoveredServices, err := k8s.DiscoverServices(config.Kubernetes)
		if err != nil {
			return err
		}
		config.Services = append(config.Services, discoveredServices...)
		log.Printf("[config][validateKubernetesConfig] Discovered %d Kubernetes services", len(discoveredServices))
	}
	return nil
}

func validateServicesConfig(config *Config) error {
	for _, service := range config.Services {
		if config.Debug {
			log.Printf("[config][validateServicesConfig] Validating service '%s'", service.Name)
		}
		if err := service.ValidateAndSetDefaults(); err != nil {
			return err
		}
	}
	log.Printf("[config][validateServicesConfig] Validated %d services", len(config.Services))
	return nil
}

func validateSecurityConfig(config *Config) error {
	if config.Security != nil {
		if config.Security.IsValid() {
			if config.Debug {
				log.Printf("[config][validateSecurityConfig] Basic security configuration has been validated")
			}
		} else {
			// If there was an attempt to configure security, then it must mean that some confidential or private
			// data are exposed. As a result, we'll force a panic because it's better to be safe than sorry.
			return ErrInvalidSecurityConfig
		}
	}
	return nil
}

// validateAlertingConfig validates the alerting configuration
// Note that the alerting configuration has to be validated before the service configuration, because the default alert
// returned by provider.AlertProvider.GetDefaultAlert() must be parsed before core.Service.ValidateAndSetDefaults()
// sets the default alert values when none are set.
func validateAlertingConfig(alertingConfig *alerting.Config, services []*core.Service, debug bool) {
	if alertingConfig == nil {
		log.Printf("[config][validateAlertingConfig] Alerting is not configured")
		return
	}
	alertTypes := []alert.Type{
		alert.TypeCustom,
		alert.TypeDiscord,
		alert.TypeMattermost,
		alert.TypeMessagebird,
		alert.TypePagerDuty,
		alert.TypeSlack,
		alert.TypeTelegram,
		alert.TypeTwilio,
	}
	var validProviders, invalidProviders []alert.Type
	for _, alertType := range alertTypes {
		alertProvider := alertingConfig.GetAlertingProviderByAlertType(alertType)
		if alertProvider != nil {
			if alertProvider.IsValid() {
				// Parse alerts with the provider's default alert
				if alertProvider.GetDefaultAlert() != nil {
					for _, service := range services {
						for alertIndex, serviceAlert := range service.Alerts {
							if alertType == serviceAlert.Type {
								if debug {
									log.Printf("[config][validateAlertingConfig] Parsing alert %d with provider's default alert for provider=%s in service=%s", alertIndex, alertType, service.Name)
								}
								provider.ParseWithDefaultAlert(alertProvider.GetDefaultAlert(), serviceAlert)
							}
						}
					}
				}
				validProviders = append(validProviders, alertType)
			} else {
				log.Printf("[config][validateAlertingConfig] Ignoring provider=%s because configuration is invalid", alertType)
				invalidProviders = append(invalidProviders, alertType)
			}
		} else {
			invalidProviders = append(invalidProviders, alertType)
		}
	}
	log.Printf("[config][validateAlertingConfig] configuredProviders=%s; ignoredProviders=%s", validProviders, invalidProviders)
}
