// Copyright 2018-2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with the License. A copy of the License is located at
//
//     http://aws.amazon.com/apache2.0/
//
// or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and limitations under the License.

package cfg

import (
	"fmt"
	"io"
	"io/ioutil"
	"os"
	"reflect"

	"github.com/aws/aws-xray-daemon/pkg/util"

	"gopkg.in/yaml.v2"

	log "github.com/cihub/seelog"
)

// Version number of the X-Ray daemon.
var Version = "unknown"

var cfgFileVersions = [...]int{1, 2} // Supported versions of cfg.yaml file.

var configLocations = []string{
	"/etc/amazon/xray/cfg.yaml",
	"cfg.yaml",
	"github.com/aws/aws-xray-daemon/pkg/cfg.yaml",
}

// LogFile represents log file passed through command line argument.
var LogFile string

// LogFormat defines format for logger.
var LogFormat = "%Date(2006-01-02T15:04:05Z07:00) [%Level] %Msg%n"

// Config defines configuration structure for cli parameters.
type Config struct {
	// Maximum buffer size in MB (minimum 3). Choose 0 to use 1% of host memory.
	TotalBufferSizeMB int `yaml:"TotalBufferSizeMB"`

	// Maximum number of concurrent calls to AWS X-Ray to upload segment documents.
	Concurrency int `yaml:"Concurrency"`

	// X-Ray service endpoint to which the daemon sends segment documents.
	Endpoint string `yaml:"Endpoint"`

	// Send segments to AWS X-Ray service in a specific region.
	Region string `yaml:"Region"`

	Socket struct {
		// Address and port on which the daemon listens for UDP packets containing segment documents.
		UDPAddress string `yaml:"UDPAddress"`
		TCPAddress string `yaml:"TCPAddress"`
	} `yaml:"Socket"`

	ProxyServer struct {
		IdleConnTimeout     int
		MaxIdleConnsPerHost int
		MaxIdleConns        int
	}

	// Structure for logging.
	Logging struct {
		// LogRotation, if true, will rotate log after 50 MB size of current log file.
		LogRotation *bool `yaml:"LogRotation"`
		// The log level, from most verbose to least: dev, debug, info, warn, error, prod (default).
		LogLevel string `yaml:"LogLevel"`
		// Logs to the specified file path.
		LogPath string `yaml:"LogPath"`
	} `yaml:"Logging"`

	// Local mode to skip EC2 instance metadata check.
	LocalMode *bool `yaml:"LocalMode"`

	// Amazon Resource Name (ARN) of the AWS resource running the daemon.
	ResourceARN string `yaml:"ResourceARN"`

	// IAM role to upload segments to a different account.
	RoleARN string `yaml:"RoleARN"`

	// Enable or disable TLS certificate verification.
	NoVerifySSL *bool `yaml:"NoVerifySSL"`

	// Upload segments to AWS X-Ray through a proxy.
	ProxyAddress string `yaml:"ProxyAddress"`

	// Daemon configuration file format version.
	Version int `yaml:"Version"`
}

// DefaultConfig returns default configuration for X-Ray daemon.
func DefaultConfig() *Config {
	return &Config{
		TotalBufferSizeMB: 0,
		Concurrency:       8,
		Endpoint:          "",
		Region:            "",
		Socket: struct {
			UDPAddress string `yaml:"UDPAddress"`
			TCPAddress string `yaml:"TCPAddress"`
		}{
			UDPAddress: "127.0.0.1:2000",
			TCPAddress: "127.0.0.1:2000",
		},
		ProxyServer: struct {
			IdleConnTimeout     int
			MaxIdleConnsPerHost int
			MaxIdleConns        int
		}{

			IdleConnTimeout:     30,
			MaxIdleConnsPerHost: 2,
			MaxIdleConns:        0,
		},
		Logging: struct {
			LogRotation *bool  `yaml:"LogRotation"`
			LogLevel    string `yaml:"LogLevel"`
			LogPath     string `yaml:"LogPath"`
		}{
			LogRotation: util.Bool(true),
			LogLevel:    "prod",
			LogPath:     "",
		},
		LocalMode:    util.Bool(false),
		ResourceARN:  "",
		RoleARN:      "",
		NoVerifySSL:  util.Bool(false),
		ProxyAddress: "",
		Version:      1,
	}
}

// ParameterConfig is a configuration used by daemon.
type ParameterConfig struct {
	SegmentChannel struct {
		// Size of trace segments channel.
		Std int
	}

	Socket struct {
		// Socket buffer size.
		BufferSizeKB int
	}

	// Number of go routines daemon.poll() to spawn.
	ReceiverRoutines int

	Processor struct {
		// Size of the batch segments processed by Processor.
		BatchSize int

		// Idle timeout in milliseconds used while sending batch segments.
		IdleTimeoutMillisecond int

		// MaxIdleConnPerHost, controls the maximum idle
		// (keep-alive) HTTP connections to keep per-host.
		MaxIdleConnPerHost int

		// Used to set Http client timeout in seconds.
		RequestTimeout          int
		BatchProcessorQueueSize int
	}
}

// ParameterConfigValue returns instance of ParameterConfig, initialized with default values.
var ParameterConfigValue = &ParameterConfig{
	SegmentChannel: struct {
		Std int
	}{
		Std: 250,
	},
	Socket: struct {
		BufferSizeKB int
	}{
		BufferSizeKB: 64,
	},
	ReceiverRoutines: 2,
	Processor: struct {
		BatchSize               int
		IdleTimeoutMillisecond  int
		MaxIdleConnPerHost      int
		RequestTimeout          int
		BatchProcessorQueueSize int
	}{
		BatchSize:               50,
		IdleTimeoutMillisecond:  1000,
		MaxIdleConnPerHost:      8,
		RequestTimeout:          2,
		BatchProcessorQueueSize: 20,
	},
}

// LoadConfig returns configuration from a valid configFile else default configuration.
func LoadConfig(configFile string) *Config {
	if configFile == "" {
		for _, val := range configLocations {
			if _, err := os.Stat(val); os.IsNotExist(err) {
				continue
			}
			return merge(val)
		}
		return DefaultConfig()
	}
	return merge(configFile)
}

func loadConfigFromFile(configPath string) *Config {
	bytes, err := ioutil.ReadFile(configPath)
	if err != nil {
		errorAndExit("", err)
	}
	return loadConfigFromBytes(bytes)
}

func loadConfigFromBytes(bytes []byte) *Config {
	c := &Config{}
	err := yaml.Unmarshal(bytes, c)
	if err != nil {
		errorAndExit("", err)
	}
	return c
}

func errorAndExit(serr string, err error) {
	createLogWritersAndLog(serr, err)
	rescueStderr := os.Stderr
	_, w, _ := os.Pipe()
	os.Stderr = w

	w.Close()
	os.Stderr = rescueStderr
	os.Exit(1)
}

// createLogWritersAndLog writes to stderr and provided log file.
func createLogWritersAndLog(serr string, err error) {
	var stderrWriter = os.Stderr
	var writer io.Writer

	stderrLogger, _ := log.LoggerFromWriterWithMinLevelAndFormat(stderrWriter, log.ErrorLvl, LogFormat)
	writeToLogger(stderrLogger, serr, err)

	if LogFile == "" {
		return
	}
	writer, _ = log.NewFileWriter(LogFile)
	fileLogger, _ := log.LoggerFromWriterWithMinLevelAndFormat(writer, log.ErrorLvl, LogFormat)
	writeToLogger(fileLogger, serr, err)
}

func writeToLogger(fileLogger log.LoggerInterface, serr string, err error) {
	log.ReplaceLogger(fileLogger)
	if serr != "" {
		log.Errorf("%v", serr)
	} else if err != nil {
		log.Errorf("Error occur when using config flag: %v", err)
	}
}

func configFlagArray(config yaml.MapSlice) []string {
	var configArray []string
	for i := 0; i < len(config); i++ {
		if config[i].Value == nil || reflect.TypeOf(config[i].Value).String() != "yaml.MapSlice" {
			configArray = append(configArray, fmt.Sprint(config[i].Key))
		} else {
			configItem := yaml.MapSlice{}
			configItem = config[i].Value.(yaml.MapSlice)
			for j := 0; j < len(configItem); j++ {
				configArray = append(configArray, fmt.Sprintf("%v.%v", config[i].Key, configItem[j].Key))
			}
		}
	}
	return configArray
}

func validConfigArray() []string {
	validConfig := yaml.MapSlice{}
	validConfigBytes, verr := yaml.Marshal(DefaultConfig())
	if verr != nil {
		errorAndExit("", verr)
	}
	yerr := yaml.Unmarshal(validConfigBytes, &validConfig)
	if yerr != nil {
		errorAndExit("", yerr)
	}
	return configFlagArray(validConfig)
}

func userConfigArray(configPath string) []string {
	fileBytes, rerr := ioutil.ReadFile(configPath)
	if rerr != nil {
		errorAndExit("", rerr)
	}
	userConfig := yaml.MapSlice{}
	uerr := yaml.Unmarshal(fileBytes, &userConfig)
	if uerr != nil {
		errorAndExit("", uerr)
	}
	return configFlagArray(userConfig)
}

// ConfigValidation validates provided configuration file, invalid configuration will exit the process.
func ConfigValidation(configPath string) {
	validConfigArray := validConfigArray()
	userConfigArray := userConfigArray(configPath)

	notSupportFlag := []string{"Profile.CPU", "Profile.Memory", "Socket.BufferSizeKB", "Logging.LogFormat", "Processor.BatchProcessorQueueSize"}
	needMigrateFlag := []string{"LogRotation", "Processor.Region", "Processor.Endpoint", "Processor.Routine", "MemoryLimit"}
	for i := 0; i < len(userConfigArray); i++ {
		if !contains(userConfigArray, "Version") {
			errorAndExit("Config Version is missing. Use X-Ray Daemon Config Migration Script to update the config file. Please refer to AWS X-Ray Documentation for more information.", nil)
		}
		if !contains(validConfigArray, userConfigArray[i]) {
			if contains(notSupportFlag, userConfigArray[i]) {
				errorMessage := fmt.Sprintf("%v flag is not supported any more. Please refer to AWS X-Ray Documentation for more information.", userConfigArray[i])
				errorAndExit(errorMessage, nil)
			} else if contains(needMigrateFlag, userConfigArray[i]) {
				errorMessage := fmt.Sprintf("%v flag is not supported. Use X-Ray Daemon Config Migration Script to update the config file. Please refer to AWS X-Ray Documentation for more information.", userConfigArray[i])
				errorAndExit(errorMessage, nil)
			} else {
				errorMessage := fmt.Sprintf("%v flag is invalid. Please refer to AWS X-Ray Documentation for more information.", userConfigArray[i])
				errorAndExit(errorMessage, nil)
			}
		}
	}
}

func contains(s []string, e string) bool {
	for _, a := range s {
		if a == e {
			return true
		}
	}
	return false
}

func merge(configFile string) *Config {
	userConfig := loadConfigFromFile(configFile)
	versionMatch := false
	for i := 0; i < len(cfgFileVersions); i++ {
		if cfgFileVersions[i] == userConfig.Version {
			versionMatch = true
			break
		}
	}

	if !versionMatch {
		errorAndExit("Config Version Setting is not correct. Use X-Ray Daemon Config Migration Script to update the config file. Please refer to AWS X-Ray Documentation for more information.", nil)
	}

	userConfig.Socket.UDPAddress = getStringValue(userConfig.Socket.UDPAddress, DefaultConfig().Socket.UDPAddress)
	userConfig.Socket.TCPAddress = getStringValue(userConfig.Socket.TCPAddress, DefaultConfig().Socket.TCPAddress)
	userConfig.ProxyServer.IdleConnTimeout = DefaultConfig().ProxyServer.IdleConnTimeout
	userConfig.ProxyServer.MaxIdleConnsPerHost = DefaultConfig().ProxyServer.MaxIdleConnsPerHost
	userConfig.ProxyServer.MaxIdleConns = DefaultConfig().ProxyServer.MaxIdleConns
	userConfig.TotalBufferSizeMB = getIntValue(userConfig.TotalBufferSizeMB, DefaultConfig().TotalBufferSizeMB)
	userConfig.ResourceARN = getStringValue(userConfig.ResourceARN, DefaultConfig().ResourceARN)
	userConfig.RoleARN = getStringValue(userConfig.RoleARN, DefaultConfig().RoleARN)
	userConfig.Concurrency = getIntValue(userConfig.Concurrency, DefaultConfig().Concurrency)
	userConfig.Endpoint = getStringValue(userConfig.Endpoint, DefaultConfig().Endpoint)
	userConfig.Region = getStringValue(userConfig.Region, DefaultConfig().Region)
	userConfig.Logging.LogRotation = getBoolValue(userConfig.Logging.LogRotation, DefaultConfig().Logging.LogRotation)
	userConfig.Logging.LogLevel = getStringValue(userConfig.Logging.LogLevel, DefaultConfig().Logging.LogLevel)
	userConfig.Logging.LogPath = getStringValue(userConfig.Logging.LogPath, DefaultConfig().Logging.LogPath)
	userConfig.NoVerifySSL = getBoolValue(userConfig.NoVerifySSL, DefaultConfig().NoVerifySSL)
	userConfig.LocalMode = getBoolValue(userConfig.LocalMode, DefaultConfig().LocalMode)
	userConfig.ProxyAddress = getStringValue(userConfig.ProxyAddress, DefaultConfig().ProxyAddress)
	return userConfig
}

func getStringValue(configValue string, defaultValue string) string {
	if configValue == "" {
		return defaultValue
	}
	return configValue
}

func getIntValue(configValue, defaultValue int) int {
	if configValue == 0 {
		return defaultValue
	}
	return configValue
}

func getBoolValue(configValue, defaultValue *bool) *bool {
	if configValue == nil {
		return defaultValue
	}
	return configValue
}
