/*
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",
		"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")
}

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

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

func createClients() error {
	if Resolve("rollbar-token") != "" {
		rollbarErrorLogger = rollbar.NewErrorLogger(Resolve("rollbar-token"), Resolve("environment"))
	}

	if Resolve("statsd-host-port") != "" && Resolve("app") != "" && Resolve("app") != "_test" {
		hostname := sanitizedHostname()

		region := Resolve("aws-region")
		if region == "" && MultiRegionMetricsEnabled() {
			return fmt.Errorf("Multi region metrics were enabled but AWS_REGION wasn't specified")
		}

		var namespace string
		if MultiRegionMetricsEnabled() {
			namespace = fmt.Sprintf("%s.%s.%s", Resolve("app"), Resolve("environment"), region)
		} else {
			namespace = fmt.Sprintf("%s.%s", Resolve("app"), Resolve("environment"))
		}

		statsRoot, err := statsd.NewBufferedClient(Resolve("statsd-host-port"), namespace, time.Second, 0)
		if err != nil {
			return err
		}

		statsCluster = statsRoot.NewSubStatter("all").(*statsd.Client)
		statsHost = statsRoot.NewSubStatter(hostname).(*statsd.Client)
	}

	return 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
}
