package sandstorm

import (
	"bytes"
	"fmt"
	"log"
	"os"
	"strconv"
	"strings"
	"syscall"
	"text/template"

	"io/ioutil"

	"path/filepath"

	"code.justin.tv/d8a/buddy/lib/config"
	multierror "github.com/hashicorp/go-multierror"
	"github.com/pkg/errors"
)

func Reload() error {
	pid, err := getAgentPid()
	if err != nil {
		return errors.Wrap(err, "could not retrieve process id for sandstorm-agent")
	}

	process, err := os.FindProcess(pid)
	if err != nil {
		return errors.Wrap(err, fmt.Sprintf("could not retrieve process id %d for sandstorm-agent", pid))
	}

	err = process.Signal(syscall.SIGHUP)
	if err != nil {
		return errors.Wrap(err, fmt.Sprintf("failed to issue SIGHUP to sandstorm-agent process"))
	}

	return nil
}

func getAgentPid() (int, error) {
	fileBytes, err := ioutil.ReadFile("/var/run/sandstorm-agent.pid")
	if err != nil {
		return 0, err
	}

	return strconv.Atoi(strings.TrimSpace(string(fileBytes)))
}

func UpdateTemplate(cluster *config.Cluster, username string, secretPath string) error {
	collectedErrors := new(multierror.Error)

	secretText, err := templateText(secretTemplate, cluster.Name, username, secretPath)
	if err != nil {
		collectedErrors = multierror.Append(collectedErrors, err)
	}

	configText, err := templateText(configTemplate, cluster.Name, username, secretPath)
	if err != nil {
		collectedErrors = multierror.Append(collectedErrors, err)
	}

	if collectedErrors.ErrorOrNil() != nil {
		return collectedErrors.ErrorOrNil()
	}

	_, err = syncSandstormPair([]byte(configText), []byte(secretText), fmt.Sprintf("rds-buddy-%s-%s", cluster.Name, username))
	return err
}

func RemoveTemplate(cluster *config.Cluster, username string) error {
	_, err := syncSandstormPair([]byte(""), []byte(""), fmt.Sprintf("rds-buddy-%s-%s", cluster.Name, username))

	return err
}

func deadClusterFilePath(clusters map[string]*config.Cluster, pathPrefix string, pathSuffix string) (bool, error) {
	collectedErrors := new(multierror.Error)
	changed := false

	files, err := filepath.Glob(pathPrefix + "*" + pathSuffix)
	if err != nil {
		collectedErrors = multierror.Append(collectedErrors, errors.Wrap(err, "could not retrieve files from glob for dead clusters"))
	}

	for _, file := range files {
		//Retrieve cluster & user
		doRemove := false
		clusterUser := strings.TrimSuffix(strings.TrimPrefix(file, pathPrefix), pathSuffix)
		clusterEndIndex := strings.Index(clusterUser, "-")
		if clusterEndIndex >= 0 {
			clusterName := clusterUser[:clusterEndIndex]
			_, ok := clusters[clusterName]
			if !ok {
				doRemove = true
			}
		}

		//If the file path is garbage or belongs to a cluster that no longer exists, remove it
		if doRemove {
			log.Printf("Removing '%s'\n", file)
			fileChanged, err := syncFlatFile([]byte(""), file, false, 0700)
			changed = changed || fileChanged

			if err != nil {
				collectedErrors = multierror.Append(collectedErrors, errors.Wrap(err, fmt.Sprintf("could not remove file '%s' from disk", file)))
			}
		}
	}

	return changed, collectedErrors.ErrorOrNil()
}

func RemoveDeadClusterTemplates(clusters map[string]*config.Cluster) (bool, error) {
	changed := false
	collectedErrors := new(multierror.Error)

	confChanged, err := deadClusterFilePath(clusters, "/etc/sandstorm-agent/conf.d/rds-buddy-", ".conf")
	changed = changed || confChanged
	if err != nil {
		collectedErrors = multierror.Append(collectedErrors, err)
	}

	confChanged, err = deadClusterFilePath(clusters, "/etc/sandstorm-agent/templates.d/rds-buddy-", "")
	changed = changed || confChanged
	if err != nil {
		collectedErrors = multierror.Append(collectedErrors, err)
	}

	return changed, collectedErrors.ErrorOrNil()
}

func syncSingleClusterTemplate(templ *template.Template, sandstormClient SandstormAPI, cluster *config.Cluster, dbUsers map[string]bool, templatePrefix string, templateSuffix string) (bool, error) {
	foundUsers := make(map[string]bool)
	collectedErrors := new(multierror.Error)
	changed := false
	configUsers := make(map[string]*config.User)

	for _, user := range cluster.User {
		configUsers[user.Name] = user
	}

	//Remove dead configs
	files, err := filepath.Glob(templatePrefix + "*" + templateSuffix)
	if err != nil {
		collectedErrors = multierror.Append(collectedErrors, errors.Wrap(err, fmt.Sprintf("could not retrieve config files from path for cluster %s", cluster.Name)))
	}

	for _, file := range files {
		//Retrieve username
		username := strings.TrimSuffix(strings.TrimPrefix(file, templatePrefix), templateSuffix)
		_, isInDb := dbUsers[username]
		configUser, isInConfig := configUsers[username]

		secretExists := sandstormClient.SecretExists(configUser.Secret)

		if isInConfig && isInDb && secretExists {
			foundUsers[username] = true
		} else {
			foundUsers[username] = false
			err = RemoveTemplate(cluster, username)
			if err != nil {
				collectedErrors = multierror.Append(collectedErrors, errors.Wrap(err, fmt.Sprintf("could not remove sandstorm templates for user %s in cluster %s", username, cluster.Name)))
			} else {
				changed = true
			}
		}
	}

	//Update templates
	for _, user := range cluster.User {
		shouldExist, found := foundUsers[user.Name]

		if !found {
			_, isInDb := dbUsers[user.Name]
			secretExists := sandstormClient.SecretExists(user.Secret)
			shouldExist = isInDb && secretExists
		}

		if !shouldExist {
			continue
		}

		secretText, secretErr := templateText(secretTemplate, cluster.Name, user.Name, user.Secret)
		if secretErr != nil {
			collectedErrors = multierror.Append(collectedErrors, secretErr)
		}

		configText, configErr := templateText(configTemplate, cluster.Name, user.Name, user.Secret)
		if configErr != nil {
			collectedErrors = multierror.Append(collectedErrors, configErr)
		}

		if secretErr == nil && configErr == nil {
			fileChanged, err := syncSandstormPair([]byte(configText), []byte(secretText), fmt.Sprintf("rds-buddy-%s-%s", cluster.Name, user.Name))
			changed = changed || fileChanged
			if err != nil {
				collectedErrors = multierror.Append(collectedErrors, err)
			}
		}
	}

	return changed, collectedErrors.ErrorOrNil()
}

func SyncClusterTemplates(sandstormClient SandstormAPI, cluster *config.Cluster, dbUsers map[string]bool) (bool, error) {
	var changed bool
	collectedErrors := new(multierror.Error)

	confChanged, err := syncSingleClusterTemplate(configTemplate, sandstormClient, cluster, dbUsers, fmt.Sprintf("/etc/sandstorm-agent/conf.d/rds-buddy-%s-", cluster.Name), ".conf")
	changed = changed || confChanged
	if err != nil {
		collectedErrors = multierror.Append(collectedErrors, err)
	}

	confChanged, err = syncSingleClusterTemplate(secretTemplate, sandstormClient, cluster, dbUsers, fmt.Sprintf("/etc/sandstorm-agent/templates.d/rds-buddy-%s-", cluster.Name), "")
	changed = changed || confChanged
	if err != nil {
		collectedErrors = multierror.Append(collectedErrors, err)
	}

	return changed, collectedErrors.ErrorOrNil()
}

func syncSandstormPair(confText []byte, templateText []byte, fileName string) (bool, error) {
	changed := false
	combinedErrors := new(multierror.Error)

	if strings.TrimSpace(string(confText)) == "" || strings.TrimSpace(string(templateText)) == "" {
		confText = []byte("")
		templateText = []byte("")
	}

	fileChanged, err := syncFlatFile(templateText, fmt.Sprintf("/etc/sandstorm-agent/templates.d/%s", fileName), false, 0700)
	changed = changed || fileChanged
	if err != nil {
		combinedErrors = multierror.Append(combinedErrors, err)
	}

	fileChanged, err = syncFlatFile(confText, fmt.Sprintf("/etc/sandstorm-agent/conf.d/%s.conf", fileName), false, 0700)
	changed = changed || fileChanged
	if err != nil {
		combinedErrors = multierror.Append(combinedErrors, err)
	}

	return changed, combinedErrors.ErrorOrNil()
}

func syncFlatFile(text []byte, path string, writeEmpty bool, filemode os.FileMode) (changed bool, err error) {
	combinedErr := new(multierror.Error)
	changed = false

	currentText, err := ioutil.ReadFile(path)
	if err != nil && !os.IsNotExist(err) {
		combinedErr = multierror.Append(combinedErr, errors.Wrap(err, fmt.Sprintf("could not read '%s' from file", path)))
	} else {
		if !writeEmpty && err == nil && strings.TrimSpace(string(text)) == "" {
			err = os.Remove(path)
			if err != nil {
				combinedErr = multierror.Append(combinedErr, errors.Wrap(err, fmt.Sprintf("could not remove '%s' from disk", path)))
			} else {
				changed = true
			}
		} else if (writeEmpty || strings.TrimSpace(string(text)) != "") && (err != nil || string(text) != string(currentText)) {
			err = ioutil.WriteFile(path, text, filemode)
			if err != nil {
				combinedErr = multierror.Append(combinedErr, errors.Wrap(err, fmt.Sprintf("could not write '%s' config to file", path)))
			} else {
				changed = true
			}
		}
	}

	err = combinedErr.ErrorOrNil()
	return
}

func WritePasswordFiles(clusters config.ClusterList) (bool, error) {
	combinedErr := new(multierror.Error)
	changed := false

	pgpassTemplateText, err := passTemplateText(pgpassTemplate, clusters)
	if err != nil {
		combinedErr = multierror.Append(combinedErr, err)
	} else {
		fileChanged, err := syncSandstormPair(pgpassConf, []byte(pgpassTemplateText), "rds-buddy-pgpass")
		changed = changed || fileChanged
		if err != nil {
			combinedErr = multierror.Append(combinedErr, err)
		}
	}

	mycnfTemplateText, err := passTemplateText(mycnfTemplate, clusters)
	if err != nil {
		combinedErr = multierror.Append(combinedErr, err)
	} else {
		fileChanged, err := syncSandstormPair(mycnfConf, []byte(mycnfTemplateText), "rds-buddy-myncf")
		changed = changed || fileChanged
		if err != nil {
			combinedErr = multierror.Append(combinedErr, err)
		}
	}

	return changed, combinedErr.ErrorOrNil()
}

var configTemplate *template.Template
var secretTemplate *template.Template
var pgpassTemplate *template.Template
var mycnfTemplate *template.Template
var pgpassConf []byte
var mycnfConf []byte

func passTemplateText(tmp *template.Template, clusters config.ClusterList) (string, error) {
	var output bytes.Buffer
	err := tmp.Execute(&output, struct {
		Clusters config.ClusterList
	}{
		Clusters: clusters,
	})
	if err != nil {
		return "", errors.Wrap(err, fmt.Sprintf("could not execute template %s", tmp.Name()))
	}

	return output.String(), nil
}

func templateText(tmp *template.Template, clusterName string, username string, secretPath string) (string, error) {
	var output bytes.Buffer
	err := tmp.Execute(&output, struct {
		UserName    string
		ClusterName string
		Secret      string
	}{
		UserName:    username,
		ClusterName: clusterName,
		Secret:      secretPath,
	})
	if err != nil {
		return "", errors.Wrap(err, fmt.Sprintf("could not execute template %s with clusterName=%s, username=%s, secretPath=%s", tmp.Name(), clusterName, username, secretPath))
	}

	return output.String(), nil
}

func init() {
	confTemplateText, err := Asset("templates/conf.tmpl")
	if err != nil {
		panic(err)
	}
	configTemplate, err = template.New("sandstorm/templates/conf.tmpl").Parse(string(confTemplateText))
	if err != nil {
		panic(err)
	}
	secretTemplateText, err := Asset("templates/secret.tmpl")
	if err != nil {
		panic(err)
	}
	secretTemplate, err = template.New("sandstorm/templates/secret.tmpl").Parse(string(secretTemplateText))
	if err != nil {
		panic(err)
	}

	pgpassTemplateText, err := Asset("templates/pgpass.tmpl")
	if err != nil {
		panic(err)
	}

	pgpassTemplate, err = template.New("sandstorm/templates/pgpass.tmpl").Parse(string(pgpassTemplateText))
	if err != nil {
		panic(err)
	}

	myconfTemplateText, err := Asset("templates/mycnf.tmpl")
	if err != nil {
		panic(err)
	}

	mycnfTemplate, err = template.New("sandstorm/templates/mycnf.tmpl").Parse(string(myconfTemplateText))
	if err != nil {
		panic(err)
	}

	pgpassConf, err = Asset("templates/pgpass.conf")
	if err != nil {
		panic(err)
	}

	mycnfConf, err = Asset("templates/mycnf.conf")
	if err != nil {
		panic(err)
	}
}
