package main

import (
	"context"
	"encoding/json"
	"os"
	"os/exec"
	"os/signal"
	"sync"
	"time"

	"math/rand"

	"code.justin.tv/dta/necronomicon-agent/agent"
	"github.com/goamz/goamz/aws"
	"github.com/goamz/goamz/s3"
	"github.com/spf13/cobra"
	"github.com/udhos/equalfile"
)

func init() {
	daemonCmd := &cobra.Command{
		Use:     "daemon",
		Short:   "Runs in daemon mode",
		Long:    `Runs as daemon, watching for configuration deployments and reloading services`,
		PreRunE: loadConfig,
		RunE:    runDaemon,
	}
	cmd.AddCommand(daemonCmd)
}

func runDaemon(cmd *cobra.Command, args []string) error {
	if daemonConfiguration.S3.Profile != "" {
		os.Setenv("AWS_PROFILE", daemonConfiguration.S3.Profile)
	}
	auth, _ := aws.GetAuth("", "", "", time.Time{})
	region := aws.Regions[daemonConfiguration.S3.Region]
	s3 := s3.New(auth, region)
	bucket := s3.Bucket(daemonConfiguration.S3.Bucket)
	agent.SetLogger(log)
	a := &agent.Agent{
		Bucket: bucket,
		Logger: func(msg string, err error) {
			log.Errorf("%s - %v", msg, err)
		},
		CheckInterval: daemonConfiguration.S3.Interval,
	}
	var wg sync.WaitGroup
	for name, service := range daemonConfiguration.Services {
		n := name
		s := service
		a.CallbackOnDeployedEnvironment(service.Environment, service.Namespaces, func(deployedEnvironment agent.DeployedEnvironment) {
			if s.Delay > 0 {
				r := rand.Int() % s.Delay
				log.Infof("change detected - service:%v, environment:%v - wait for random delay %v/%v", n, s.Environment, r, s.Delay)
				time.Sleep(time.Duration(r) * time.Second)
			}
			wg.Add(1)
			deployEnvironmentToService(n, s, &deployedEnvironment)
			wg.Done()
		})
		log.Debugf("registered - service:%v, environment:%v", n, s.Environment)
	}
	go func() {
		a.WatchForDeployments()
	}()
	signalChan := make(chan os.Signal, 1)
	signal.Notify(signalChan, os.Interrupt)
	<-signalChan
	a.StopWatchingForDeployments()
	wg.Wait()
	return nil
}

func deployEnvironmentToService(serviceName string, service *necroServiceConfigurationEntry, deployedEnvironment *agent.DeployedEnvironment) {
	updated, err := updateJSONFileIfDifferent(service.File, makeNamespaceValues(service.Namespaces, deployedEnvironment.Namespaces))
	if err != nil {
		log.Errorf("could not write file for service %s environment %s deployment %d: %v", serviceName, deployedEnvironment.Environment, deployedEnvironment.ID, err)
		return
	}
	if updated {
		log.Debug(deployedEnvironment)
		log.Infof("updated %s - service:%v, environment:%v, deployment#:%d", service.File, serviceName, deployedEnvironment.Environment, deployedEnvironment.ID)
		if err := runReloadCommand(service.Reload, service.Timeout, deployedEnvironment.ID); err != nil {
			log.Errorf("could not run reload command for service %s environment %s deployment %d: %v", serviceName, deployedEnvironment.Environment, deployedEnvironment.ID, err)
			return
		}
	}
}

func updateJSONFileIfDifferent(path string, values map[string]map[string]interface{}) (bool, error) {
	tmppath := path + ".tmp"
	f, err := os.OpenFile(tmppath, os.O_CREATE|os.O_WRONLY, 0644)
	if err != nil {
		return false, err
	}
	defer os.Remove(tmppath)
	encoder := json.NewEncoder(f)
	if err := encoder.Encode(values); err != nil {
		f.Close()
		return false, err
	}
	f.Close()
	cmp := equalfile.New(nil, equalfile.Options{})
	same, _ := cmp.CompareFile(tmppath, path)
	if !same {
		if err := os.Rename(tmppath, path); err != nil {
			return false, err
		}
	}
	return !same, nil
}

func runReloadCommand(reload string, timeout int, deploymentID int) error {
	var ctx context.Context
	var cancel context.CancelFunc
	if timeout <= 0 {
		ctx, cancel = context.WithCancel(context.Background())
	} else {
		ctx, cancel = context.WithTimeout(context.Background(), time.Second*time.Duration(timeout))
	}
	defer cancel()
	cmd := exec.CommandContext(ctx, "sh", "-c", reload)
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	err := cmd.Start()
	if err != nil {
		return err
	}
	err = cmd.Wait()
	if err != nil {
		return err
	}
	return nil
}

// returned value reuses the nested maps, so modifying the returned maps would change the maps of the original values.
func makeNamespaceValues(namespaces []string, values map[string]map[string]interface{}) map[string]map[string]interface{} {
	if len(namespaces) == 0 {
		return values
	}
	newvalues := make(map[string]map[string]interface{})
	for _, ns := range namespaces {
		if values[ns] == nil {
			continue
		}
		newvalues[ns] = values[ns]
	}
	return newvalues
}
