package agent

import (
	"encoding/json"
	"errors"
	"fmt"
	"io/ioutil"
	"log/syslog"
	"net"
	"net/http"
	"net/url"
	"os"
	"path"
	"reflect"
	"strconv"
	"strings"
	"time"

	"code.justin.tv/systems/sandstorm/manager"

	"code.justin.tv/systems/sandstorm/queue"
	log "github.com/Sirupsen/logrus"
	"github.com/Sirupsen/logrus/hooks/syslog"
	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/credentials"
	"github.com/aws/aws-sdk-go/aws/credentials/stscreds"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/sts"
	"github.com/cactus/go-statsd-client/statsd"
	"github.com/hashicorp/hcl"
)

// Handles loading and dealing with configuration options

const (
	sectionHeader = "sandstorm-agent"
	// ConfigFolderName is the folder that'll contain the main config files
	ConfigFolderName = "conf.d"
	// QueueConfigFileName is the name of the file for storing the SQS configuration
	QueueConfigFileName = "queue_arn"
	// StateFileName is the default name for the Sandstorm State File
	StateFileName              = "sandstorm_state"
	queueNamePrefixDefault     = "sandstorm"
	defaultRegion              = "us-west-2"
	defaultDwell               = 5
	defaultInitialInterval     = 1
	defaultBackoffMultiplier   = 2
	defaultMaxWaitTime         = 60
	defaultStatsdHost          = "statsd.internal.justin.tv:8125"
	defaultTestSecretPlaintext = "THIS_IS_A_TEST_SECRET"
)

var (
	// queueNamePrefix is used for creating SQS subscribers for SNS
	queueNamePrefix = queueNamePrefixDefault

	sandstormAccountNumber = "734326455073"
)

// Build creates an agent from it's specified configurations, loads templates,
// initializes logging and finally prepares AWS queues, KMS, etc
func Build(configFolderPath string, createLogger bool, isRun bool) (*Agent, error) {

	err := errIfNotAbs(configFolderPath)
	if err != nil {
		return nil, err
	}

	// Load base configuration
	agent, err := loadAgent(configFolderPath, isRun)
	if err != nil {
		return nil, err
	}

	if agent.Logging && createLogger {
		// TODO: Make this configurable
		syslogHook, err := logrus_syslog.NewSyslogHook("udp", "localhost:514", syslog.LOG_LOCAL4, "sandstorm-agent")
		if err != nil {
			return agent, err
		}
		log.AddHook(syslogHook)
	}

	awsConfig := agent.createAWSConfig()
	agent.DefaultQueue, agent.defaultManager, err = agent.configureAWS(awsConfig)
	if err != nil {
		if isSystemError(err) && agent.Proxy != "" && agent.Proxy != "none" {
			log.Infof("Failed to connect to SQS, failing over to use proxy: %s", agent.Proxy)
			proxyAwsConfig, err := agent.createProxyAWSConfig()
			if err != nil {
				return agent, err
			}
			agent.DefaultQueue, agent.defaultManager, err = agent.configureAWS(proxyAwsConfig)
			if err != nil {
				return nil, err
			}
		} else {
			return nil, err
		}
	}
	agent.Queue = agent.DefaultQueue
	agent.manager = agent.defaultManager

	if agent.Proxy != "" && agent.Proxy != "none" {
		agent.useProxy = true
		proxyAwsConfig, err := agent.createProxyAWSConfig()
		if err != nil {
			return agent, err
		}
		agent.ProxyQueue, agent.proxyManager, err = agent.configureAWS(proxyAwsConfig)
		if err != nil {
			return nil, err
		}
	}

	log.Info("Agent loaded from configuration")
	return agent, nil
}

// BuildSimulatorAgent builds a bare minimum agent with all secrets set to defaultTestSecretPlaintext
func BuildSimulatorAgent(configFolderPath string) (*Agent, error) {
	err := errIfNotAbs(configFolderPath)
	if err != nil {
		return nil, err
	}

	// Load base configuration
	agent, err := loadAgent(configFolderPath, true)
	if err != nil {
		return nil, err
	}

	simulatorValues, err := loadSimulatorValues(configFolderPath)
	if err != nil {
		log.Info("Error loading sandstorm_simulate.json, will default to dummy values")
		err = nil
	}

	for _, template := range agent.State.Templates {
		for _, secret := range template.Secrets {
			// have to type assert the interface to set dummy values
			// defaults to dummy string otherwise
			if dummyValue, ok := simulatorValues[secret.Name]; ok {
				switch v := dummyValue.(type) {
				case int:
					secret.Plaintext = []byte(strconv.Itoa(v))
				case float64:
					secret.Plaintext = []byte(strconv.Itoa(int(v)))
				case bool:
					secret.Plaintext = []byte(strconv.FormatBool(v))
				case string:
					secret.Plaintext = []byte(v)
				default:
					fmt.Printf("Unsupported type %v\n", reflect.TypeOf(v))
				}
			} else {
				secret.Plaintext = []byte(defaultTestSecretPlaintext)
			}
		}
	}

	return agent, err

}

// loadAgent builds an agent from a specified configuration path and attempts
// to initialize and load the various states it cares about
func loadAgent(configFolderPath string, isRun bool) (*Agent, error) {

	agent, err := agentFromConfig(configFolderPath)
	if err != nil {
		return nil, err
	}

	templates, err := loadTemplates(agent.configFolderPath)
	if err != nil {
		if e, ok := err.(NoConfiguredTemplatesError); ok {
			log.Warn(e)
			err = nil
			templates = make([]*Template, 0)
		} else {
			return nil, err
		}
	}

	stateFilePath := path.Join(agent.configFolderPath, StateFileName)
	if !isRun {
		err = syncWithStateFile(stateFilePath, templates)
		if err != nil {
			return nil, err
		}
	}
	// Finally add templates to the agent
	for _, template := range templates {
		err = agent.AddTemplate(template)
		if err != nil {
			return nil, err
		}
	}

	return agent, nil
}

// loadSimulaterValues loads the dummy values provided in sandstorm_simulate.json for secrets and stores
// them in a map
func loadSimulatorValues(configFolderPath string) (simulateValues map[string]interface{}, err error) {
	simulatorFile := path.Join(configFolderPath, "sandstorm_simulate.json")
	simulatorBytes, err := ioutil.ReadFile(simulatorFile)
	if err != nil {
		return
	}
	err = json.Unmarshal(simulatorBytes, &simulateValues)
	return
}

// agentFromConfig reads an agent in from a configuration folder and returns it
func agentFromConfig(configFolderPath string) (*Agent, error) {

	err := errIfNotAbs(configFolderPath)
	if err != nil {
		return nil, err
	}

	agent := New()
	agent.configFolderPath = configFolderPath

	agentConfigString, err := buildConfigString(agent.configFolderPath)
	if err != nil {
		return nil, err
	}

	err = hcl.Decode(agent, agentConfigString)
	if err != nil {
		return nil, err
	}

	if agent.TopicARN == "" {
		err = fmt.Errorf("configuration error: missing TopicARN")
		return nil, err
	}
	if agent.TableName == "" {
		err = fmt.Errorf("configuration error: missing TableName")
		return nil, err
	}
	if agent.Region == "" {
		log.Infof("no Region set, defaulting to %s", defaultRegion)
		agent.Region = defaultRegion
	}
	if agent.Dwell == 0 {
		log.Infof("no Dwell set, defaulting to %s", defaultDwell)

		agent.Dwell = defaultDwell
	}

	if agent.BackoffMultiplier == 0 {
		log.Infof("no Backoff set, defaulting to %d", defaultBackoffMultiplier)

		agent.BackoffMultiplier = defaultBackoffMultiplier
	}

	if agent.MaxWaitTime == 0 {
		log.Infof("no maxWaitTime set, defaulting to %d", defaultMaxWaitTime)

		agent.MaxWaitTime = defaultMaxWaitTime
	}

	agent.sandstormAccountNumber = sandstormAccountNumber

	return agent, nil
}

// syncWithStateFile attempts to load an Agent State file (which is created after
// every successful agent run) and will compare the state of it's templates to those
// that we've loaded, resulting in any templates that have changes to be marked as
// dirty.
//
// // Note: this does not necessarily mean that secrets within it are in sync, it simply
// means that the previous state of the outputted template has not changed
func syncWithStateFile(stateFilePath string, templates []*Template) error {

	if fileExists(stateFilePath) {

		stateFile, err := parseStateFile(stateFilePath)
		if err != nil {
			return fmt.Errorf("Error loading statefile: %s", err)
		}

		log.Infof("State file load from %s.", stateFilePath)

		// compare loaded templates (marked dirty by default) to those of the state
		// file (in a clean state).
		for index, template := range templates {
			for _, sTemplate := range stateFile.Templates {
				if template.IsEqual(sTemplate) {
					// just replace the whole thing
					templates[index] = sTemplate
				}
			}
		}

		log.Info("Agent synced with state file.")
	}

	return nil
}

// custom roundtripper to embed statter
type roundTripper struct {
	http.RoundTripper
	stats statsd.Statter
	pop   string
}

// intializeds roundTripper
func newRoundTripper(rt http.RoundTripper, pop string) (http.RoundTripper, error) {
	stats, err := statsd.NewClient(defaultStatsdHost, "sandstorm.production")
	if err != nil {
		return nil, err
	}
	return &roundTripper{
		RoundTripper: rt,
		stats:        stats,
		pop:          pop,
	}, nil
}

// calls underlying http.RoundTripper and sends metrics to graphite via statsd
func (rt *roundTripper) RoundTrip(Request *http.Request) (*http.Response, error) {

	//time stamp
	currentTime := time.Now()

	// do request
	resp, err := rt.RoundTripper.RoundTrip(Request)

	// timestamp

	//send stat
	metricsErr := rt.stats.Inc(fmt.Sprintf("%s.proxy.requests", rt.pop), 1, 1.0)
	if metricsErr != nil {
		log.Error(err)
	}
	metricsErr = rt.stats.Timing(fmt.Sprintf("%s.proxy.requestTime", rt.pop), int64(time.Since(currentTime).Seconds()*1000), 1.0)
	if metricsErr != nil {
		log.Error(err)
	}

	return resp, err
}

// Sets agent to use proxy, goroutine will reset to use normal HTTP client after 900 seconds
func (a *Agent) setProxyUse(useProxy bool) {
	if useProxy && a.Proxy != "" && a.Proxy != "none" && a.useProxy && (a.manager != a.proxyManager || a.Queue != a.ProxyQueue) {
		log.Infof("Swapping to use Proxy %s", a.Proxy)
		a.manager = a.proxyManager
		a.Queue = a.ProxyQueue
		time.AfterFunc(time.Second*900, func() {
			a.setProxyUse(false)
		})
	} else if useProxy == false {
		log.Info("Stopping Proxy use")
		a.manager = a.defaultManager
		a.Queue = a.DefaultQueue
	}
}

// Creates awsConfig for use with Amazon services based, replacing default HTTP client if
// indicated.
func (a *Agent) createAWSConfig() *aws.Config {
	return &aws.Config{Region: aws.String(a.Region)}
}

// Creates awsConfig using proxy HTTP client from base awsConfig
func (a *Agent) createProxyAWSConfig() (awsConfig *aws.Config, err error) {
	awsConfig = a.createAWSConfig()

	proxyURL, err := url.Parse(a.Proxy)
	if err != nil {
		return nil, err
	}

	DefaultTransport := &http.Transport{
		Proxy: http.ProxyURL(proxyURL),
		DialContext: (&net.Dialer{
			Timeout:   30 * time.Second,
			KeepAlive: 30 * time.Second,
		}).DialContext,
		MaxIdleConns:          100,
		IdleConnTimeout:       90 * time.Second,
		TLSHandshakeTimeout:   10 * time.Second,
		ExpectContinueTimeout: 1 * time.Second,
	}

	rT, err := newRoundTripper(DefaultTransport, a.Pop)
	if err != nil {
		return nil, err
	}

	proxyHTTPClient := &http.Client{Transport: rT}
	awsConfig.HTTPClient = proxyHTTPClient

	return

}

//Configures the SQS service and Sandstorm Manager with provided awsConfig
func (a *Agent) configureAWS(awsConfig *aws.Config) (Queue *queue.Queue, sandstormManager manager.API, err error) {

	if a.RoleARN != "default" && a.RoleARN != "" {
		stsclient := sts.New(session.New(awsConfig))
		arp := &stscreds.AssumeRoleProvider{
			Duration:     900 * time.Second,
			ExpiryWindow: 10 * time.Second,
			RoleARN:      a.RoleARN,
			Client:       stsclient,
		}

		credentials := credentials.NewCredentials(arp)
		awsConfig.WithCredentials(credentials)
	}

	stsclient := sts.New(session.New(awsConfig))
	callerIdentity, err := stsclient.GetCallerIdentity(&sts.GetCallerIdentityInput{})
	if err != nil {
		return
	}

	if aws.StringValue(callerIdentity.Account) != a.sandstormAccountNumber {
		err = &IncorrectAWSAccount{accountNumber: aws.StringValue(callerIdentity.Account)}
		return
	}

	Queue, err = queue.New(queue.Config{
		TopicArn:        a.TopicARN,
		QueueConfigPath: a.configFolderPath,
		QueueFileName:   QueueConfigFileName,
		QueueNamePrefix: queueNamePrefix,
		AWSConfig:       awsConfig,
	})

	if err != nil {
		return
	}
	sandstormManager = manager.New(manager.Config{
		AWSConfig: awsConfig,
		TableName: a.TableName,
		KeyID:     a.KeyID,
	})
	return
}

func buildConfigString(configFolderPath string) (string, error) {
	var byteConfig []byte
	configPartialsFolder := path.Join(configFolderPath, ConfigFolderName)

	// TEMPORARY: If the conf.d folder doesn't exist, assume we've upgraded without
	// migrating configs to the new style and support that for a while.
	// Use-case is the sandstorm-agent package getting upgraded without puppet
	// applying the new style.
	if _, err := os.Stat(configPartialsFolder); os.IsNotExist(err) {
		oldConfigFile := path.Join(configFolderPath, "config")
		log.Warnf("WARNING: Using deprecated single-file configuration at %s. Please migrate to conf.d-style configuration folder %s.", oldConfigFile, configPartialsFolder)

		byteConfig, err = ioutil.ReadFile(oldConfigFile)
		if err != nil {
			return "", err
		}

		return string(byteConfig), nil
	}
	// End TEMPORARY

	// Retrieve all files in configFolderPath so we can concatinate them
	configFiles, err := ioutil.ReadDir(configPartialsFolder)
	if err != nil {
		return "", err
	}

	for _, file := range configFiles {
		// Only parse files that end with .conf
		if !strings.HasSuffix(file.Name(), ".conf") {
			continue
		}

		fullPath := path.Join(configPartialsFolder, file.Name())
		fileBytes, err := ioutil.ReadFile(fullPath)
		if err != nil {
			return "", err
		}

		byteConfig = append(byteConfig[:], fileBytes[:]...)
	}

	return string(byteConfig), nil
}

// LoadTemplates attempts to load Secret Templates from file, doesn't actually
// populate/output the keys to file
func loadTemplates(configFolderPath string) ([]*Template, error) {

	// var out map[string]interface{}
	// config := struct {
	// Template []*Template
	// }{}

	agentConfigString, err := buildConfigString(configFolderPath)
	if err != nil {
		return nil, err
	}

	config := make(map[string]interface{})

	err = hcl.Decode(&config, agentConfigString)
	if err != nil {
		return nil, fmt.Errorf(
			"Error parsing sandstorm configuration\n%s\nError: %s",
			agentConfigString,
			err.Error(),
		)
	}

	var ok bool
	var rawTemplates interface{}
	if rawTemplates, ok = config["template"]; !ok {
		return nil, NoConfiguredTemplatesError{}
	}

	var confTemplates []map[string]interface{}
	if confTemplates, ok = rawTemplates.([]map[string]interface{}); !ok {
		return nil, errors.New("Unable to parse configuration templates")
	}

	if len(confTemplates) == 0 {
		return nil, NoConfiguredTemplatesError{}
	}

	err = setDefaultIDs()
	if err != nil {
		log.Info("Error getting current user, templates will default to root/root if user/group are not provided.")
	}

	templates := make([]*Template, len(confTemplates))

	// Super ghetto struct mapping due to above mentioned issue with HCL
	for i, temp := range confTemplates {

		// required params
		source := temp["source"].(string)
		destination := temp["destination"].(string)
		command := temp["command"].(string)

		template := NewTemplate(source, destination, command)

		if temp["user"] != nil {

			user := temp["user"].(string)
			uid, err := getUserID(user)
			if err != nil {
				return nil, fmt.Errorf("error initializing template %s: user %s does not exist", source, user)
			}

			template.UserID = uid
		}

		if temp["group"] != nil {

			groupName := temp["group"].(string)
			gid, err := getGroupID(groupName)
			if err != nil {
				return nil, fmt.Errorf("error initializing template %s: %s", source, err.Error())
			}

			template.GroupID = gid
		}

		if temp["mode"] != nil {

			strMode, isStr := temp["mode"].(string)
			if !isStr {
				return nil, fmt.Errorf("unexpected type '%T' for file mode", temp["mode"])
			}

			mode, err := strconv.ParseUint(strMode, 8, 32)
			if err != nil {
				return nil, fmt.Errorf("error converting file mode to uint from %s", strMode)
			}

			template.SetFileMode(mode)
		}

		err = template.Initialize()
		if err != nil {
			return nil, fmt.Errorf("error initializing template %s: %s", template.Source, err.Error())
		}

		templates[i] = template
	}

	// Old, better logic dependant on HCL bug to be fixed:
	// https://github.com/hashicorp/hcl/issues/60
	//For each template, parse it's file contents from the provided "source"
	// for _, template := range config.Template {
	// err := template.Initialize(a)
	// if err != nil {
	// return err
	// }

	// a.addTemplate(template)
	// }

	return templates, nil
}

// CreatePIDFile manages custom PID file creation for complex restart requirements
func CreatePIDFile(pIDPath string) error {

	err := os.MkdirAll(path.Dir(pIDPath), FolderPermissions)
	if err != nil {
		return err
	}

	f, err := os.Create(pIDPath)
	if err != nil {
		return err
	}
	defer func() {
		err := f.Close()
		if err != nil {
			log.Error(err.Error())
		}
	}()

	_, err = f.WriteString(strconv.Itoa(os.Getpid()))
	if err != nil {
		return err
	}

	return f.Sync()
}

// ExitSignalHandler handles any incoming system signals
func ExitSignalHandler(c chan os.Signal, pidPath string) {
	s := <-c
	log.Info("Received signal: " + s.String())
	ExitandCleanUp(pidPath)

}

func removePID(pidPath string) error {
	if !fileExists(pidPath) {
		return nil
	}
	return os.RemoveAll(pidPath)
}

// ExitandCleanUp removes pidfile and calls os.Exit(0)
func ExitandCleanUp(pidPath string) {
	err := removePID(pidPath)
	if err != nil {
		log.Fatal(err)
	}
	log.Info("Exiting...")
	os.Exit(0)
}
