/*
Package config enables packages to define configuration keys with defaults. These keys are read in during application start from either command-line flags or environment variables.

Since the configuration keys are defined per-package, this allows applications which import packages integrating with common/config to automatically become configurable without adding boilerplate code in main.go.
*/
package config

import (
	"flag"
	"fmt"
	"os"
	"regexp"
	"strings"
	"time"

	"code.justin.tv/common/golibs/errorlogger/rollbar"
	"code.justin.tv/common/golibs/pkgpath"
	"github.com/cactus/go-statsd-client/statsd"
)

var (
	options            map[string]map[string]*string
	rollbarErrorLogger *rollbar.ErrorLogger
	statsCluster       statsd.Statter
	statsHost          statsd.Statter
	invalidStatRegexp  = regexp.MustCompile(`[^A-Za-z0-9-]+`)
)

func init() {
	main, _ := pkgpath.Main()
	sub := strings.Split(main, "/")
	Register(map[string]string{
		"environment":          "development",
		"statsd-host-port":     "statsd.central.twitch.a2z.com:8125",
		"rollbar-token":        "",
		"app":                  sub[len(sub)-1],
		"aws-region":           "",
		"multi-region-metrics": "",
	})

	statsCluster, _ = statsd.NewNoopClient()
	statsHost, _ = statsd.NewNoopClient()
}

// Register accepts a map of configuration keys with defaults. These fields only apply to the package which registers them.
func Register(fields map[string]string) {
	if options == nil {
		options = make(map[string]map[string]*string)
	}

	pkg, _ := pkgpath.Caller(1)

	if options[pkg] == nil {
		options[pkg] = make(map[string]*string)
	}

	for k, v := range fields {
		options[pkg][k] = flag.String(k, v, "")
	}
}

// Parse should be called by the main package to ensure all package-level configurations have been registered.
func Parse() error {
	flag.Parse()

	for _, opts := range options {
		for k := range opts {
			envVal, ok := checkEnvForField(k)
			if ok {
				opts[k] = &envVal
			}
		}
	}

	err := createClients()
	if err != nil {
		return err
	}

	return nil
}

// Resolve returns the configured value for a given key, based on command-line flags or environment variables.
// If a key cannot be resolved, it returns a blank string. Resolve must only be called after Parse.
func Resolve(field string) string {
	pkg, _ := pkgpath.Caller(1)
	return resolveWithPackage(field, pkg)
}

func resolveWithPackage(field, pkg string) string {
	packageOptions := options[pkg]
	resolved, ok := packageOptions[field]
	if !ok {
		return ""
	}
	return *resolved
}

// MustResolve returns the configured value for a given key using Resolve. If a key cannot be resolved, a panic is
// invoked. MustResolve must only be called after Parse.
func MustResolve(field string) string {
	pkg, _ := pkgpath.Caller(1)
	resolved := resolveWithPackage(field, pkg)
	if len(resolved) == 0 {
		message := fmt.Sprintf("\nMissing required config argument: %s\n\n", field)
		panic(message)
	}
	return resolved
}

// MustResolveDuration returns a time.Duration parsed from a config path.
// You specify durations a la ParseDuration: 100ms , 4m30s, 2h30m, etc.
// If the key cannot be resolved or if the key resolves to an invalid duration
// string, a panic will be raised.
//
// NOTE hours are the largest unit specifyable (no days, months, years, etc).
// a time.Duration is capable of specifying down to the nanosecond (ns) level so
// this should work for basically any sensible uses like timeouts, thresholds,
// repeating timers in rather short intervals.
func MustResolveDuration(field string) time.Duration {
	pkg, _ := pkgpath.Caller(1)
	resolved := resolveWithPackage(field, pkg)
	duration, err := time.ParseDuration(strings.ToLower(resolved))
	if err != nil {
		panic(err)
	}
	return duration
}

// App returns the app name (as reported to statsd) of the current application.
func App() string {
	return Resolve("app")
}

// Environment returns the Twitch-specific deployment environment of the current application
func Environment() string {
	return Resolve("environment")
}

// MultiRegionMetricsEnabled returns whether or not multi region metrics are enabled
func MultiRegionMetricsEnabled() bool {
	val := strings.ToLower(Resolve("multi-region-metrics"))
	return val == "enabled"
}

// StatsdHostPort returns the address to send statsd metrics
func StatsdHostPort() string {
	return Resolve("statsd-host-port")
}

// Statsd returns a ready-to-use stats.Statter to report application metrics with the hostname included. During test runs, this will return a Noop stats.Statter.
func Statsd() statsd.Statter {
	return statsHost
}

// SetStatsd overrides the existing Statter with something new.  Call this after parse, or it will
// modify what is set by Parse.  Useful for migration plans that involve custom metrics agents.
func SetStatsd(s statsd.Statter) {
	statsHost = s
}

// ClusterStatsd returns a ready-to-use stats.Statter to report application metrics without a hostname. During test runs, this will return a Noop stats.Statter.
func ClusterStatsd() statsd.Statter {
	return statsCluster
}

// SetClusterStatsd overrides the existing Cluster Statter with something new.  Call this after parse, or it will
// modify what is set by Parse. Useful for migration plans that involve custom metrics agents.
func SetClusterStatsd(s statsd.Statter) {
	statsCluster = s
}

// RollbarErrorLogger returns a ready-to-use client for reporting errors. If the "rollbar-token" configuration is blank, this is nil.
func RollbarErrorLogger() *rollbar.ErrorLogger {
	return rollbarErrorLogger
}

// AwsRegion returns the aws-region
func AwsRegion() string {
	return Resolve("aws-region")
}

// RollbarToken returns the rollbar-token
func RollbarToken() string {
	return Resolve("rollbar-token")
}

type validationError interface {
	IsValidationErr() bool
}

type validationErr struct {
	message string
}

func (err validationErr) IsValidationErr() bool { return true }
func (err validationErr) Error() string         { return err.message }

func isValidationError(err error) bool {
	if err == nil {
		return false
	}

	vErr, ok := err.(validationError)
	if !ok {
		return false
	}

	return vErr.IsValidationErr()
}
func sanitizedHostname() string {
	hostname, err := os.Hostname()
	if err != nil {
		return "unknown"
	}

	return invalidStatRegexp.ReplaceAllString(hostname, "_")
}

func createClients() error {

	if err := SetRollbarClient(RollbarConf{
		Token:       RollbarToken(),
		Environment: Environment(),
	}); err != nil {
		return err
	}

	if err := SetStatsClients(StatsConf{
		HostPort:           StatsdHostPort(),
		App:                App(),
		Environment:        Environment(),
		AWSRegion:          AwsRegion(),
		MultiRegionMetrics: MultiRegionMetricsEnabled(),
	}); err != nil {
		return err
	}
	return nil
}

// RollbarConf describes the configuration used by a rollbar client.
type RollbarConf struct {
	Token       string
	Environment string
}

// Validate validates whether the configuration is valid and can be used to configure a rollbar client.
func (c RollbarConf) Validate() error {
	if c.Token == "" {
		return validationErr{message: "must provide rollbar token"}
	}

	return nil
}

// SetRollbarClient sets the global rollbar client using the given configuration. If a validation error
// is encountered, it is ignored and the global client is not set.
func SetRollbarClient(conf RollbarConf) error {
	var err error
	rollbarErrorLogger, err = initRollbarClient(conf)
	if err == nil || isValidationError(err) {
		return nil
	}
	return err
}

func initRollbarClient(conf RollbarConf) (*rollbar.ErrorLogger, error) {
	if err := conf.Validate(); err != nil {
		return nil, err
	}

	return rollbar.NewErrorLogger(conf.Token, conf.Environment), nil
}

// StatsConf describes the configuration used by a statsd client.
type StatsConf struct {
	HostPort           string
	Environment        string
	App                string
	AWSRegion          string
	MultiRegionMetrics bool
}

// Validate validates whether the configuration is valid and can be used to configure statsd clients. If a validation
// error is encountered, it is ignored and the global client is not set.
func (c StatsConf) Validate() error {
	if c.HostPort == "" {
		return validationErr{message: "statsd host port cannot be empty"}
	}

	if c.App == "" {
		return validationErr{message: "statsd app name cannot be empty"}
	}

	if c.App == "_test" {
		return validationErr{message: "invalid app name \"_test\""}
	}

	if c.MultiRegionMetrics && c.AWSRegion == "" {
		return validationErr{message: "Multi region metrics were enabled but AWS_REGION wasn't specified"}
	}
	return nil
}

// SetStatsClients sets the cluster and host statsd clients using the given configuration.
func SetStatsClients(conf StatsConf) error {
	var err error
	statsCluster, statsHost, err = initStatsClients(conf)
	if err == nil || isValidationError(err) {
		return nil
	}

	return err
}

func initStatsClients(conf StatsConf) (statsd.Statter, statsd.Statter, error) {
	if err := conf.Validate(); err != nil {
		return nil, nil, err
	}

	hostName := sanitizedHostname()

	var namespace string
	if conf.MultiRegionMetrics {
		namespace = fmt.Sprintf("%s.%s.%s", conf.App, conf.Environment, conf.AWSRegion)
	} else {
		namespace = fmt.Sprintf("%s.%s", conf.App, conf.Environment)
	}

	statsRoot, err := statsd.NewBufferedClient(conf.HostPort, namespace, time.Second, 0)
	if err != nil {
		return nil, nil, err
	}

	cluster := statsRoot.NewSubStatter("all").(*statsd.Client)
	host := statsRoot.NewSubStatter(hostName).(*statsd.Client)

	return cluster, host, nil
}

func checkEnvForField(key string) (string, bool) {
	key = strings.Replace(strings.ToUpper(key), "-", "_", -1)

	val := os.Getenv(key)
	if val == "" {
		return "", false
	}

	return val, true
}
