package main

import (
	"errors"
	"fmt"
	"log"
	"os"
	"path"
	"strings"
	"unicode"

	"github.com/aws/aws-sdk-go/aws/credentials/stscreds"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/ecs"
)

var (
	adminPanelID = map[string]string{
		"staging":    "219087926005",
		"production": "196915980276",
		"canary":     "196915980276",
	}
	desiredHostCount = map[string]int64{
		"staging":    1,
		"canary":     1,
		"production": 8,
	}
	subnets = map[string][]*string{
		"staging": {
			aws.String("subnet-0cadddd78b4c89c78"),
			aws.String("subnet-095a737b8c52df148"),
			aws.String("subnet-0273de04e1e1d8d5c"),
		},
		"production": {
			aws.String("subnet-0a51cda3754e805ed"),
			aws.String("subnet-044bcd89fa58bb83c"),
			aws.String("subnet-01fd426b9040c1901"),
		},
		"canary": {
			aws.String("subnet-0a51cda3754e805ed"),
			aws.String("subnet-044bcd89fa58bb83c"),
			aws.String("subnet-01fd426b9040c1901"),
		},
	}
	adminPanelEcrURL = map[string]string{
		"staging":    "219087926005.dkr.ecr.us-west-2.amazonaws.com/admin-panel-staging",
		"production": "196915980276.dkr.ecr.us-west-2.amazonaws.com/admin-panel-production",
		"canary":     "196915980276.dkr.ecr.us-west-2.amazonaws.com/admin-panel-canary",
	}
	s2sServiceNames = map[string]string{
		"staging":    "admin-panel-staging",
		"production": "admin-panel-production",
		"canary":     "admin-panel-production",
	}
)

// UpdateTaskDef ...
type UpdateTaskDef struct {
	Environment string
	GitCommit   string
	Ecs         *ecs.ECS
}

func newUpdateTaskDef() (*UpdateTaskDef, error) {
	env := os.Getenv("ENVIRONMENT")
	if env == "" {
		return nil, errors.New("ENVIRONMENT must be set")
	}

	gitCommit := os.Getenv("GIT_COMMIT")
	if gitCommit == "" {
		return nil, errors.New("GIT_COMMIT must be set")
	}

	assumeRoleCreds := stscreds.NewCredentials(session.New(&aws.Config{Region: aws.String("us-west-2")}), fmt.Sprintf(
		"arn:aws:iam::%s:role/admin-panel-deploy-%s",
		adminPanelID[env],
		env,
	))

	return &UpdateTaskDef{
		Environment: env,
		GitCommit:   gitCommit,
		Ecs: ecs.New(session.New(&aws.Config{
			Region:      aws.String("us-west-2"),
			Credentials: assumeRoleCreds,
		})),
	}, nil
}

func (svc *UpdateTaskDef) getExecRoleArn() *string {
	return aws.String(fmt.Sprintf(
		"arn:aws:iam::%s:role/admin-panel-ecs-exec-%s",
		adminPanelID[svc.Environment],
		svc.Environment,
	))
}

func (svc *UpdateTaskDef) getTaskRoleArn() *string {
	return aws.String(fmt.Sprintf(
		"arn:aws:iam::%s:role/admin-panel-%s",
		adminPanelID[svc.Environment],
		svc.Environment,
	))
}

func quoteFields() func(rune) bool {

	// keep track of a quote while scanning through
	quoteTracker := rune(0)
	return func(currentCharacter rune) bool {
		switch {
		case currentCharacter == quoteTracker:
			quoteTracker = rune(0)
			return false
		case quoteTracker != rune(0):
			return false
		case unicode.In(currentCharacter, unicode.Quotation_Mark):
			quoteTracker = currentCharacter
			return false
		default:
			return unicode.IsSpace(currentCharacter)
		}
	}
}

func createCommandString(command string) []*string {
	commands := strings.FieldsFunc(command, quoteFields())
	output := make([]*string, len(commands))

	for i, subcmd := range commands {
		output[i] = aws.String(subcmd)
	}
	return output
}

func (svc UpdateTaskDef) createAdminPanelBaseContainer() *ecs.ContainerDefinition {
	return &ecs.ContainerDefinition{
		Image:     aws.String(fmt.Sprintf("%s:%s", adminPanelEcrURL[svc.Environment], svc.GitCommit)),
		Essential: aws.Bool(true),
		Environment: []*ecs.KeyValuePair{
			{
				Name:  aws.String("RAILS_ENV"),
				Value: aws.String(svc.Environment),
			},
			{
				Name:  aws.String("ENVIRONMENT"),
				Value: aws.String(svc.Environment),
			},
			{
				Name:  aws.String("SECRETS_FILE_PATH"),
				Value: aws.String("/opt/twitch/admin-panel/etc/secrets.json"),
			},
			{
				Name:  aws.String("RBENV_ROOT"),
				Value: aws.String("/home/jtv/.rbenv/"),
			},
			{
				Name:  aws.String("RACK_ENV"),
				Value: aws.String(svc.Environment),
			},
			{
				Name:  aws.String("STATSD_HOST_PORT"),
				Value: aws.String("localhost:8125"),
			},
			{
				Name:  aws.String("QUEUE"),
				Value: aws.String("*"),
			},
		},
		VolumesFrom: []*ecs.VolumeFrom{
			{
				SourceContainer: aws.String("s2s"),
			},
		},
	}
}

func (svc UpdateTaskDef) createAdminPanelServerContainer() *ecs.ContainerDefinition {
	return svc.createAdminPanelBaseContainer().
		SetCpu(2048).
		SetMemory(4096).
		SetName(fmt.Sprintf("admin-panel-%s", svc.Environment)).
		SetCommand(createCommandString("./scripts/rails-launcher.sh")).
		SetLogConfiguration(&ecs.LogConfiguration{
			LogDriver: aws.String("awslogs"),
			Options: map[string]*string{
				"awslogs-group":         aws.String(fmt.Sprintf("admin-panel-%s", svc.Environment)),
				"awslogs-region":        aws.String("us-west-2"),
				"awslogs-stream-prefix": aws.String("rails-app"),
			},
		}).
		SetPortMappings([]*ecs.PortMapping{
			{ContainerPort: aws.Int64(8000)},
		})
}

func (svc UpdateTaskDef) createAdminPanelWorkerContainer() *ecs.ContainerDefinition {
	return svc.createAdminPanelBaseContainer().
		SetCpu(1024).
		SetMemory(1024).
		SetName("rails-worker").
		SetCommand(createCommandString("bundle exec rake resque:work --trace -j 10")).
		SetLogConfiguration(&ecs.LogConfiguration{
			LogDriver: aws.String("awslogs"),
			Options: map[string]*string{
				"awslogs-group":         aws.String(fmt.Sprintf("admin-panel-%s", svc.Environment)),
				"awslogs-region":        aws.String("us-west-2"),
				"awslogs-stream-prefix": aws.String("rails-worker"),
			},
		})
}

func (svc *UpdateTaskDef) createS2SSidecarContainer() *ecs.ContainerDefinition {
	return &ecs.ContainerDefinition{
		Image:     aws.String(fmt.Sprintf("%s:%s-s2s", adminPanelEcrURL[svc.Environment], svc.GitCommit)),
		Cpu:       aws.Int64(1024),
		Essential: aws.Bool(true),
		Memory:    aws.Int64(1024),
		Name:      aws.String("s2s"),
		Environment: []*ecs.KeyValuePair{
			{
				Name:  aws.String("S2SSIDECAR_CALLERNAME"),
				Value: aws.String(s2sServiceNames[svc.Environment]),
			},
			{
				Name:  aws.String("S2SSIDECAR_PORT"),
				Value: aws.String(svc.s2sSidecarTCPPort()),
			},
		},
		LogConfiguration: &ecs.LogConfiguration{
			LogDriver: aws.String("awslogs"),
			Options: map[string]*string{
				"awslogs-group":         aws.String(fmt.Sprintf("admin-panel-%s", svc.Environment)),
				"awslogs-region":        aws.String("us-west-2"),
				"awslogs-stream-prefix": aws.String("s2s"),
			},
		},
		MountPoints: []*ecs.MountPoint{
			{
				SourceVolume:  aws.String(svc.s2sSidecarSocketVolumeName()),
				ContainerPath: aws.String(svc.s2sSidecarSocketDirectory()),
			},
		},
	}
}

func (svc *UpdateTaskDef) createSandstormContainer() *ecs.ContainerDefinition {
	return &ecs.ContainerDefinition{
		Image:     svc.sandstormImage(),
		Essential: aws.Bool(true),
		Name:      aws.String("sandstorm"),
		Command: []*string{
			aws.String("serve"),
			aws.String("-addr"), aws.String("127.0.0.1:6900"),
			aws.String("-role"), aws.String(svc.sandstormRoleArn()),
		},
		LogConfiguration: &ecs.LogConfiguration{
			LogDriver: aws.String("awslogs"),
			Options: map[string]*string{
				"awslogs-group":         aws.String(fmt.Sprintf("admin-panel-%s", svc.Environment)),
				"awslogs-region":        aws.String("us-west-2"),
				"awslogs-stream-prefix": aws.String("sandstorm"),
			},
		},
	}
}

func (svc *UpdateTaskDef) makeTaskDef() *ecs.RegisterTaskDefinitionInput {
	return &ecs.RegisterTaskDefinitionInput{
		ContainerDefinitions: []*ecs.ContainerDefinition{
			svc.createAdminPanelServerContainer(),
			svc.createAdminPanelWorkerContainer(),
			svc.createS2SSidecarContainer(),
			svc.createSandstormContainer(),
		},
		Cpu:              aws.String("4096"),
		Memory:           aws.String("8192"),
		Family:           aws.String(fmt.Sprintf("admin-panel-%s", svc.Environment)),
		ExecutionRoleArn: svc.getExecRoleArn(),
		NetworkMode:      aws.String("awsvpc"),
		TaskRoleArn:      svc.getTaskRoleArn(),
		Volumes: []*ecs.Volume{
			{Name: aws.String(svc.s2sSidecarSocketVolumeName())},
		},
	}
}

func (svc *UpdateTaskDef) register() (*ecs.RegisterTaskDefinitionOutput, error) {
	return svc.Ecs.RegisterTaskDefinition(svc.makeTaskDef())
}

func (svc *UpdateTaskDef) update(taskRoleArn *string) (*ecs.UpdateServiceOutput, error) {
	return svc.Ecs.UpdateService(&ecs.UpdateServiceInput{
		Cluster: aws.String(fmt.Sprintf("admin-panel-%s", svc.Environment)),
		DeploymentConfiguration: &ecs.DeploymentConfiguration{
			MaximumPercent:        aws.Int64(200),
			MinimumHealthyPercent: aws.Int64(100),
		},
		Service:        aws.String(fmt.Sprintf("admin-panel-%s", svc.Environment)),
		TaskDefinition: taskRoleArn,
		DesiredCount:   aws.Int64(desiredHostCount[svc.Environment]),
		NetworkConfiguration: &ecs.NetworkConfiguration{
			AwsvpcConfiguration: &ecs.AwsVpcConfiguration{
				AssignPublicIp: aws.String("DISABLED"),
				Subnets:        subnets[svc.Environment],
				SecurityGroups: []*string{
					svc.twitchSecurityGroup(),
					svc.redisSecurityGroup(),
					svc.memcacheSecurityGroup(),
					svc.vpcEndpointsSecurityGroup(),
					svc.vpcEndpointsNoSSLSecurityGroup(),
				},
			},
		},
	})
}

func (svc *UpdateTaskDef) updateHTTPRedirect() (*ecs.UpdateServiceOutput, error) {
	taskDef, err := svc.Ecs.RegisterTaskDefinition(&ecs.RegisterTaskDefinitionInput{
		ContainerDefinitions: []*ecs.ContainerDefinition{
			{
				Image:     aws.String(fmt.Sprintf("%s:%s-http", adminPanelEcrURL[svc.Environment], svc.GitCommit)),
				Essential: aws.Bool(true),
				Name:      aws.String("main"),
				LogConfiguration: &ecs.LogConfiguration{
					LogDriver: aws.String("awslogs"),
					Options: map[string]*string{
						"awslogs-group":         aws.String(fmt.Sprintf("admin-panel-%s", svc.Environment)),
						"awslogs-region":        aws.String("us-west-2"),
						"awslogs-stream-prefix": aws.String("http-redirect"),
					},
				},
				PortMappings: []*ecs.PortMapping{
					{ContainerPort: aws.Int64(80)},
				},
			},
		},
		Cpu:              aws.String("256"),
		Memory:           aws.String("512"),
		Family:           aws.String(fmt.Sprintf("admin-panel-http-redirect-%s", svc.Environment)),
		ExecutionRoleArn: svc.getExecRoleArn(),
		NetworkMode:      aws.String("awsvpc"),
	})

	if err != nil {
		return nil, err
	}

	return svc.Ecs.UpdateService(&ecs.UpdateServiceInput{
		Cluster: aws.String(fmt.Sprintf("admin-panel-%s", svc.Environment)),
		DeploymentConfiguration: &ecs.DeploymentConfiguration{
			MaximumPercent:        aws.Int64(200),
			MinimumHealthyPercent: aws.Int64(100),
		},
		Service:        aws.String(fmt.Sprintf("admin-panel-http-redirect-%s", svc.Environment)),
		TaskDefinition: taskDef.TaskDefinition.TaskDefinitionArn,
		DesiredCount:   aws.Int64(1),
		NetworkConfiguration: &ecs.NetworkConfiguration{
			AwsvpcConfiguration: &ecs.AwsVpcConfiguration{
				AssignPublicIp: aws.String("DISABLED"),
				Subnets:        subnets[svc.Environment],
				SecurityGroups: []*string{
					svc.twitchSecurityGroup(),
				},
			},
		},
	})
}

func (svc UpdateTaskDef) s2sSidecarSocketVolumeName() string {
	return "s2s_sidecar_socket"
}

func (svc UpdateTaskDef) s2sSidecarTCPPort() string {
	return "8888"
}

func (svc UpdateTaskDef) s2sSidecarSocketDirectory() string {
	return "/s2s"
}

func (svc UpdateTaskDef) s2sSidecarSocketPath() string {
	return path.Join(svc.s2sSidecarSocketDirectory(), "s2s.sock")
}

func (svc UpdateTaskDef) memcacheSecurityGroup() *string {
	switch svc.Environment {
	case "staging":
		return aws.String("sg-0f20e3809b363fda4")
	case "canary":
		return aws.String("sg-010f8021ff6b03a0f")
	case "production":
		return aws.String("sg-02aabe5a7e5e2086f")
	}
	log.Fatalf("invalid environment: %s", svc.Environment)
	return nil
}

func (svc UpdateTaskDef) redisSecurityGroup() *string {
	switch svc.Environment {
	case "staging":
		return aws.String("sg-0c87d0a99dde6b0bc")
	case "canary":
		return aws.String("sg-07f25f5e11ac3902c")
	case "production":
		return aws.String("sg-0fb67c6423a490f29")
	}
	log.Fatalf("invalid environment: %s", svc.Environment)
	return nil
}

func (svc UpdateTaskDef) twitchSecurityGroup() *string {
	switch svc.Environment {
	case "staging":
		return aws.String("sg-04f2086ae714dca07")
	case "canary":
		fallthrough
	case "production":
		return aws.String("sg-0c76f6c92e7f8890f")
	}
	log.Fatalf("invalid environment: %s", svc.Environment)
	return nil
}

func (svc UpdateTaskDef) vpcEndpointsSecurityGroup() *string {
	switch svc.Environment {
	case "staging":
		return aws.String("sg-0a1cfe9142600ad8a")
	case "canary":
		fallthrough
	case "production":
		return aws.String("sg-015f886e184a70f0e")
	}
	log.Fatalf("invalid environment: %s", svc.Environment)
	return nil
}

func (svc UpdateTaskDef) vpcEndpointsNoSSLSecurityGroup() *string {
	switch svc.Environment {
	case "staging":
		return aws.String("sg-00bacc94199b4d229")
	case "canary":
		fallthrough
	case "production":
		return aws.String("sg-063f98b8e7b4e348d")
	}
	log.Fatalf("invalid environment: %s", svc.Environment)
	return nil
}

func (svc UpdateTaskDef) sandstormEnv() string {
	switch svc.Environment {
	case "canary":
		return "production"
	}
	return svc.Environment
}

func (svc UpdateTaskDef) sandstormImage() *string {
	return aws.String(fmt.Sprintf("%s:%s-sandstorm", adminPanelEcrURL[svc.Environment], svc.GitCommit))
}

func (svc UpdateTaskDef) sandstormRoleArn() string {
	return fmt.Sprintf(
		"arn:aws:iam::734326455073:role/sandstorm/production/templated/role/admin-panel-%s",
		svc.sandstormEnv())
}

func main() {
	updateTaskDef, err := newUpdateTaskDef()
	if err != nil {
		log.Fatal(err)
	}

	registeredTask, err := updateTaskDef.register()
	if err != nil {
		log.Fatal(err)
	}

	updatedTask, err := updateTaskDef.update(registeredTask.TaskDefinition.TaskDefinitionArn)
	if err != nil {
		log.Fatal(err)
	}

	log.Printf("%s updated at %s", *updatedTask.Service.ServiceName, *updatedTask.Service.CreatedAt)

	httpRedirectService, err := updateTaskDef.updateHTTPRedirect()
	if err != nil {
		log.Fatal(err)
	}

	log.Printf("%s updated at %s", *httpRedirectService.Service.ServiceName, *httpRedirectService.Service.CreatedAt)
}
