package bootstrap

import (
	"bytes"
	"fmt"
	"log"
	"net/http"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"sync"
	"time"

	"code.justin.tv/dta/twitch-create-service/internal/bootstrap/go_http"
	"code.justin.tv/dta/twitch-create-service/internal/bootstrap/ruby_grpc"
	"code.justin.tv/dta/twitch-create-service/internal/bootstrap/ruby_http"
	"code.justin.tv/dta/twitch-create-service/internal/bootstrap/structs"
	gh "code.justin.tv/dta/twitch-create-service/internal/github"
	"code.justin.tv/dta/twitch-create-service/internal/terraform"
	"github.com/google/go-github/github"
	"github.com/hashicorp/consul/api"
)

type createdRepo struct {
	client       *github.Client
	repo         *github.Repository
	consulClient *api.Client
	verbose      bool
	app          structs.AppType
}

var AWSAccounts = map[string]map[string]string{
	"web": {
		"production": "twitch-web-aws",
		"staging":    "twitch-web-dev",
	},
	"video": {
		"production": "twitch-video-aws",
		"staging":    "twitch-video-aws",
	},
	"feeds": {
		"production": "twitch-feed-aws",
		"staging":    "twitch-feed-dev",
	},
}

var TerraformSubnetListName = map[string]string{
	"web":   "service_subnets",
	"video": "production_subnets",
	"feeds": "private_subnets",
}

func PreflightCheck(skipProvision bool, org string) error {
	accountsToCheck := make(map[string]bool)

	if !skipProvision {
		environmentsToAccounts, ok := AWSAccounts[org]
		if !ok {
			return fmt.Errorf("unknown organization: %v", org)
		}

		for _, account := range environmentsToAccounts {
			accountsToCheck[account] = true
		}
	}

	for a := range accountsToCheck {
		cmd := exec.Command("aws", "configure", "get", "aws_access_key_id", "--profile", a)
		err := cmd.Run()
		if err != nil {
			return fmt.Errorf("%v credentials not found! Run 'aws configure --profile %v' to set them", a, a)
		}

		/*
			TODO: check that we have these AWS privs for each account before continuing

			iam:CreateRole
			iam:DeleteRole
			iam:GetRole
			iam:PassRole
			iam:CreateInstanceProfile
			iam:ListInstanceProfilesForRole
			iam:GetInstanceProfile
			iam:DeleteInstanceProfile
			iam:AddRoleToInstanceProfile
			iam:RemoveRoleFromInstanceProfile

			SNS:CreateTopic
			SNS:DeleteTopic
			SNS:GetTopicAttributes
			SNS:Subscribe
			SNS:Unsubscribe
			SNS:GetSubscriptionAttributes
		*/
	}

	return nil
}

func InitializeRepository(appType string, name string, user string, token string, tmpRoot string, delete bool, verbose bool) (*createdRepo, error) {
	var app structs.AppType
	switch appType {
	case "go_http":
		app = &go_http.App{}
	case "ruby_grpc":
		app = &ruby_grpc.App{}
	case "ruby_http":
		app = &ruby_http.App{}
	default:
		return nil, fmt.Errorf("unrecognized app type: %v", appType)
	}

	log.Printf("Running %v dependency check", appType)
	err := app.PreflightCheck()
	if err != nil {
		return nil, fmt.Errorf("Missing dependencies! %v", err)
	}

	client, err := gh.GithubClient(token)
	if err != nil {
		return nil, err
	}

	repo, err := gh.CreateRepository(client, name, delete)
	if err != nil {
		return nil, err
	}

	log.Printf("%v repository available for commits", name)

	consulClient, err := api.NewClient(&api.Config{
		Address:    "consul.internal.justin.tv",
		Scheme:     "http",
		Datacenter: "us-west2",
	})

	c := &createdRepo{
		client:       client,
		repo:         repo,
		consulClient: consulClient,
		verbose:      verbose,
		app:          app,
	}

	c.app.SetClient(client, repo, verbose)

	err = c.app.MakeUsable()
	if err != nil {
		return nil, err
	}

	log.Print("Development VM ready for provisioning")

	err = c.app.MakeBuildable()
	if err != nil {
		return nil, err
	}

	log.Print("New commits will now be tested automatically")

	err = c.app.CreateApp(user, token, tmpRoot)
	if err != nil {
		return nil, err
	}
	defer cleanupTmp(tmpRoot)

	return c, nil
}

func (c *createdRepo) Name() string {
	return *c.repo.Name
}

func (c *createdRepo) FullName() string {
	return *c.repo.FullName
}

func (c *createdRepo) Owner() string {
	return *c.repo.Owner.Login
}

func (c *createdRepo) markDeployedVersion() error {
	_, commit, err := gh.GetLastestCommit(c.client, c.repo, "master")
	if err != nil {
		return err
	}

	kv := &api.KVPair{
		Key:   fmt.Sprintf("deployed-version/%v/%v/production", c.Owner(), c.Name()),
		Value: []byte(*commit.SHA),
	}
	_, err = c.consulClient.KV().Put(kv, nil)
	if err != nil {
		return err
	}

	kv = &api.KVPair{
		Key:   fmt.Sprintf("deployed-version/%v/%v/staging", c.Owner(), c.Name()),
		Value: []byte(*commit.SHA),
	}
	_, err = c.consulClient.KV().Put(kv, nil)
	if err != nil {
		return err
	}

	err = c.waitForBuild(*commit.SHA)
	if err != nil {
		return err
	}

	return nil
}

func (c *createdRepo) DefineInfrastructure(user, org string) error {
	err := c.app.CreatePuppetModule()
	if err != nil {
		return err
	}

	err = c.app.CreateTerraformFiles(user, org, AWSAccounts[org], TerraformSubnetListName[org])
	if err != nil {
		return err
	}

	log.Print("Production AWS infrastructure defined")

	// this needs to run after the last commit since jenkins will coallesce commits into a build
	err = c.markDeployedVersion()
	if err != nil {
		return err
	}

	return nil
}

func (c *createdRepo) Provision(user, token, tmpRoot string, org string) (string, error) {
	checkoutPath, err := gh.CloneLocally(user, token, tmpRoot, c.repo, c.verbose)
	if err != nil {
		return "", err
	}

	var wg sync.WaitGroup
	applyErrors := make(chan error, 2)
	wg.Add(2)

	var prodELBHost, stagingELBHost string

	go func() {
		defer wg.Done()
		var err error
		prodELBHost, err = c.runTerraform("production", AWSAccounts[org]["production"], user, token, checkoutPath, tmpRoot)
		if err != nil {
			applyErrors <- err
		}
	}()

	go func() {
		defer wg.Done()
		var err error
		stagingELBHost, err = c.runTerraform("staging", AWSAccounts[org]["staging"], user, token, checkoutPath, tmpRoot)
		if err != nil {
			applyErrors <- err
		}
	}()

	wg.Wait()
	if len(applyErrors) > 0 {
		return "", <-applyErrors
	}

	cmd := exec.Command("git", "push", "origin", "master")
	cmd.Dir = checkoutPath
	if c.verbose {
		cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr
	}
	err = cmd.Run()
	if err != nil {
		return "", fmt.Errorf("error pushing to github: %v", err)
	}

	wg.Add(2)
	waitErrors := make(chan error, 2)

	go func() {
		defer wg.Done()
		var err error
		if prodELBHost != "" {
			err = waitForELB(prodELBHost, "production")
			if err != nil {
				waitErrors <- err
			}
		}
	}()

	go func() {
		defer wg.Done()
		var err error
		if stagingELBHost != "" {
			err = waitForELB(stagingELBHost, "staging")
			if err != nil {
				waitErrors <- err
			}
		}
	}()

	wg.Wait()
	if len(waitErrors) > 0 {
		return "", <-waitErrors
	}

	// only cleanup dir if all terraform stuff succeeds. this prevents deleting tfstate files and orphaning AWS resources
	cleanupTmp(tmpRoot)

	return prodELBHost, nil
}

func (c *createdRepo) runTerraform(env, account, user, token, checkoutPath, tmpRoot string) (string, error) {
	terraformDir := filepath.Join(checkoutPath, "terraform", env)

	log.Printf("Spinning up %v instances", env)

	cmd := exec.Command("terraform", "get")
	cmd.Stderr = os.Stderr
	cmd.Dir = terraformDir
	err := cmd.Run()
	if err != nil {
		return "", err
	}

	applyEnv := os.Environ()
	applyEnv = append(applyEnv, fmt.Sprintf("AWS_PROFILE=%v", account))

	cmd = exec.Command("terraform", "apply")
	if c.verbose {
		cmd.Stdout = os.Stdout
	}
	cmd.Stderr = os.Stderr
	cmd.Dir = terraformDir
	cmd.Env = applyEnv
	err = cmd.Run()
	if err != nil {
		return "", err
	}

	var moduleOutput bytes.Buffer
	cmd = exec.Command("terraform", "output", fmt.Sprintf("-module=%v", *c.repo.Name), "elb_dns")
	cmd.Stderr = os.Stderr
	cmd.Stdout = &moduleOutput
	cmd.Dir = terraformDir
	err = cmd.Run()
	if err != nil {
		return "", err
	}

	elbHost := strings.TrimSpace(moduleOutput.String())

	err = terraform.IncrementASGCounts(filepath.Join(terraformDir, "main.tf"))
	if err != nil {
		return "", err
	}

	cmd = exec.Command("terraform", "apply")
	if c.verbose {
		cmd.Stdout = os.Stdout
	}
	cmd.Stderr = os.Stderr
	cmd.Dir = terraformDir
	cmd.Env = applyEnv
	err = cmd.Run()
	if err != nil {
		return "", err
	}

	cmd = exec.Command("git", "add", "main.tf", "terraform.tfstate")
	cmd.Dir = terraformDir
	if c.verbose {
		cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr
	}
	err = cmd.Run()
	if err != nil {
		return "", fmt.Errorf("error adding files: %v", err)
	}

	cmd = exec.Command("git", "commit", "-m", fmt.Sprintf("Provision %v environment\n\nPrepared by twitch-create-service", env))
	cmd.Dir = terraformDir
	if c.verbose {
		cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr
	}
	err = cmd.Run()
	if err != nil {
		return "", fmt.Errorf("error committing tfstate: %v", err)
	}

	return elbHost, nil
}

func (c *createdRepo) waitForBuild(sha string) error {
	log.Print("Waiting for initial build to finish")

	for {
		time.Sleep(5 * time.Second)

		statuses, _, err := c.client.Repositories.ListStatuses(*c.repo.Owner.Login, *c.repo.Name, sha, nil)
		if err != nil {
			return err
		}

		if len(statuses) == 0 {
			continue
		}

		state := *statuses[0].State

		if state == "error" || state == "failure" {
			return fmt.Errorf("jenkins build failed")
		}

		if state == "success" {
			break
		}
	}

	return nil
}

func waitForELB(host, env string) error {
	log.Printf("Waiting for %v instances to become healthy. (~15min)", env)

	limit := 180
	i := 0
	for {
		i += 1
		if i >= limit {
			return fmt.Errorf("ELB took too long to become healthy")
		}

		time.Sleep(5 * time.Second)

		resp, err := http.Get(fmt.Sprintf("http://%v/debug/running", host))
		if err != nil {
			continue
		}

		if resp.StatusCode == 200 {
			break
		}
	}

	return nil
}

func cleanupTmp(tmpRoot string) {
	if tmpRoot == "" {
		return
	}
	err := os.RemoveAll(tmpRoot)
	if err != nil {
		log.Printf("error removing tmp: %v", err)
	}
}
