package git

import (
	"bytes"
	"fmt"
	"log"
	"math/rand"
	"os"
	"os/exec"
	"strings"
	"text/template"
	"time"

	"github.com/fatih/color"
	"github.com/google/go-github/github"

	"io/ioutil"

	"code.justin.tv/d8a/buddy/lib/config"
	"code.justin.tv/d8a/buddy/lib/git/templates"
	"code.justin.tv/d8a/buddy/lib/terminal"
	"code.justin.tv/twitch/cli/ghutil"
)

var templateFiles = []string{
	"deploy.json",
	"slack.json",
	"README.md",
	"courier/post-install.sh",
	"jenkins.groovy",
}
var fileTemplates map[string]*template.Template
var dbConfTemplate *template.Template

func init() {
	fileTemplates = make(map[string]*template.Template)
	for _, file := range templateFiles {
		templateTxt, err := templates.Asset(file + ".tmpl")
		if err != nil {
			log.Fatalln(err)
		}
		template, err := template.New(file).Parse(string(templateTxt))
		if err != nil {
			log.Fatalln(err)
		}
		fileTemplates[file] = template
	}

	dbConfTxt, err := templates.Asset("dbconf.yaml.tmpl")
	if err != nil {
		log.Fatalln(err)
	}
	dbConfTemplate, err = template.New("dbconf.yaml").Parse(string(dbConfTxt))
	if err != nil {
		log.Fatalln(err)
	}
}

func EnsureRepository(config *config.ConfigFile, cluster *config.Cluster, client *github.Client) error {
	segments := strings.Split(cluster.Repository, "/")
	if len(segments) != 2 {
		return fmt.Errorf("%s is an invalid repository name", cluster.Repository)
	}

	owner := segments[0]
	repoName := segments[1]
	repoFolder := cluster.RepoFolder()
	folderExists := true
	_, err := os.Stat(repoFolder)
	if err != nil && !os.IsNotExist(err) {
		return err
	} else if err != nil {
		fmt.Printf("Can't find %s repository locally\n", cluster.Repository)
		folderExists = false
	}

	//Does the repo already exist?
	repoExists := true
	repo, resp, err := client.Repositories.Get(owner, repoName)
	if err != nil {
		if resp.StatusCode != 404 {
			return err
		}
		fmt.Printf("Repository %s does not exist on the server\n", cluster.Repository)
		repoExists = false
	}

	if !repoExists && folderExists {
		//How can the folder be real if the repo isn't real?
		err := os.RemoveAll(repoFolder)
		if err != nil {
			return err
		}
		fmt.Printf("Deleting local %s repository\n", cluster.Repository)
		folderExists = false
	}

	if !repoExists {
		fmt.Printf("Creating repository %s on server\n", cluster.Repository)
		//Create repo
		repo, _, err = client.Repositories.Create(owner, &github.Repository{
			Name:      github.String(repoName),
			Private:   github.Bool(false),
			AutoInit:  github.Bool(true),
			HasIssues: github.Bool(true),
		})

		if err != nil {
			return err
		}
		fmt.Println("Repository created")
	}

	//Verify & Commit fixes
	err = ensureRepoSetup(client, repo, cluster, config.SlackRoom, !repoExists)
	if err != nil {
		return err
	}

	if !folderExists {
		fmt.Println("Initializing local repository")

		//Clone repo
		err = os.MkdirAll(repoFolder, 0755)
		if err != nil {
			return err
		}

		cmd := exec.Command("git", "init")
		cmd.Dir = repoFolder
		cmd.Stdout = os.Stdout
		cmd.Stderr = os.Stderr
		err = cmd.Run()
		if err != nil {
			return err
		}

		fmt.Println("Successfully initialized local repository")
	}

	//We used to try and git pull this, but nuking & recreating the github would get us into a bad
	//state where we couldn't pull.  Buddy isn't really "collaborating" in the git repo, it just needs
	//to reflect whatever's live now, and we can do that with a git fetch/git reset --hard

	//Fetch
	fmt.Printf("Attempting to update %s\n", cluster.Repository)
	repoURL := fmt.Sprintf("https://%s@git-aws.internal.justin.tv/%s/%s.git", config.GithubKey, owner, repoName)
	cmd := exec.Command("git", "fetch", repoURL)
	cmd.Dir = repoFolder
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	err = cmd.Run()
	if err != nil {
		return err
	}

	cmd = exec.Command("git", "reset", "--hard", "FETCH_HEAD")
	cmd.Dir = repoFolder
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	err = cmd.Run()
	if err != nil {
		return err
	}

	if !repoExists {
		fmt.Printf("Waiting 90 seconds for the %s Jenkins task to be created.\n", cluster.Repository)
		time.Sleep(90 * time.Second)
		fmt.Printf("Wait complete- submitting a no-op commit to force a build of %s\n", cluster.Repository)

		viewModel := ViewModel{
			Name:      cluster.Name,
			Database:  cluster.Database,
			User:      cluster.SuperUser,
			SlackRoom: config.SlackRoom,
			RepoName:  *repo.Name,
			RepoOwner: *repo.Owner.Login,
		}

		writer := bytes.NewBuffer([]byte(""))
		err := fileTemplates["jenkins.groovy"].Execute(writer, viewModel)
		if err != nil {
			return err
		}

		err = ghutil.ForceCommitToBranch(client, repo, "master", "Executing no-op commit to force build.", false, &[]github.TreeEntry{
			github.TreeEntry{
				Type:    github.String("blob"),
				Content: github.String(writer.String()),
				Mode:    github.String("100644"),
				Path:    github.String("jenkins.groovy"),
			},
		})
		if err != nil {
			return err
		}
	}
	fmt.Println("Successful.")

	return nil
}

func RetrieveRepository(repository string, client *github.Client) (*github.Repository, error) {
	segments := strings.Split(repository, "/")

	owner := segments[0]
	repoName := segments[1]

	repo, resp, err := client.Repositories.Get(owner, repoName)

	if err != nil {
		if resp.StatusCode == 404 {
			return nil, nil
		}
		return nil, err
	}

	return repo, nil
}

func FindDeadRepositories(configFile *config.ConfigFile) ([]string, error) {
	var repositories []string
	repositoriesInUse := make(map[string]bool)
	for _, cluster := range configFile.Cluster {
		repositoriesInUse[cluster.Repository] = true
	}

	orgs, err := ioutil.ReadDir(config.GitRoot)
	if err != nil {
		return repositories, err
	}

	for _, org := range orgs {
		if org.IsDir() {
			repos, err := ioutil.ReadDir(config.GitRoot + org.Name())
			if err != nil {
				return repositories, err
			}

			for _, repo := range repos {
				if repo.IsDir() {
					repoName := org.Name() + "/" + repo.Name()
					_, ok := repositoriesInUse[repoName]
					if !ok {
						repositories = append(repositories, repoName)
					}
				}
			}
		}
	}

	return repositories, nil
}

func CleanupRepository(repository string, client *github.Client) error {
	repo, err := RetrieveRepository(repository, client)
	if err != nil {
		return err
	}

	if repo != nil {
		if repo.HTMLURL != nil {
			color.Yellow("Iceman repository %s is no longer in use by buddy.  Navigate to %s/settings to delete it.", repository, *repo.HTMLURL)
		} else {
			color.Yellow("Iceman repository %s is no longer in use, consider deleting it.", repository)
		}
	}

	remove, err := terminal.AskForConfirmation(fmt.Sprintf("Iceman repository %s is no longer in use by buddy.  Delete it from the local file system?", repository))
	if err != nil {
		return err
	}
	if remove {
		err := os.RemoveAll(config.GitRoot + repository + "/")
		if err != nil {
			return err
		}
	}

	return nil
}

type ViewModel struct {
	Name              string
	Database          string
	Driver            string
	User              string
	SlackRoom         string
	RepoName          string
	RepoOwner         string
	DefaultConnection string
}

func ensureRepoSetup(client *github.Client, repo *github.Repository, cluster *config.Cluster, slackRoom string, newRepo bool) error {

	//This viewmodel object is passed into the templates to provide data what needs rendering
	viewModel := ViewModel{
		Name:              cluster.Name,
		Database:          cluster.Database,
		Driver:            cluster.Driver,
		User:              cluster.SuperUser,
		SlackRoom:         slackRoom,
		RepoName:          *repo.Name,
		RepoOwner:         *repo.Owner.Login,
		DefaultConnection: defaultDbConfString(cluster),
	}

	_, tree, err := ghutil.GetBranchData(client, repo, "master")
	if err != nil {
		return err
	}

	//Round up necessary changes
	changes := make(map[string]*github.TreeEntry)
	for _, file := range templateFiles {
		if err := addFileChange(tree, changes, file, fileTemplates[file], viewModel, newRepo); err != nil {
			return err
		}
	}

	dbConfEntry, ok := tree["iceman/dbconf.yaml"]
	var dbConfPtr *github.TreeEntry
	if ok {
		dbConfPtr = &dbConfEntry
	}
	newDbConf, err := makeDbConfEntry(client, repo, dbConfPtr, viewModel, cluster)
	if err != nil {
		return err
	}
	changes["iceman/dbconf.yaml"] = newDbConf

	if len(changes) == 0 {
		return nil
	}

	return commitChanges(client, repo, changes)
}

func addFileChange(tree map[string]github.TreeEntry, changes map[string]*github.TreeEntry, file string, template *template.Template, viewModel ViewModel, newRepo bool) error {
	writer := bytes.NewBuffer([]byte(""))
	err := template.Execute(writer, viewModel)
	if err != nil {
		return err
	}

	updateFile := newRepo
	if !updateFile {
		_, ok := tree[file]
		updateFile = !ok
	}

	if updateFile {
		changes[file] = &github.TreeEntry{
			Path:    github.String(file),
			Mode:    github.String("100644"),
			Content: github.String(writer.String()),
			Type:    github.String("blob"),
		}
	}

	return nil
}

func commitChanges(client *github.Client, repo *github.Repository, changes map[string]*github.TreeEntry) error {
	fmt.Printf("Checking repository %s/%s...\n", *repo.Owner.Login, *repo.Name)
	commit, err := ghutil.CreateCommitFromChanges(client, repo, "master", changes, "Fixing iceman configuration -- RDS Buddy")
	if commit == nil || err != nil {
		return err
	}
	fmt.Println("Changes necessary.")

	//There are changes to commit
	r := rand.New(rand.NewSource(time.Now().UnixNano()))
	randBranch := fmt.Sprintf("buddy-init-%x", r.Uint32())

	title := "Configure Iceman Repo For Live DB"
	message := "--Configured by RDS buddy especially for you!"
	fmt.Println("Checking in changes to branch...")
	pr, err := ghutil.CreatePullRequest(client, repo, randBranch, "master", title, message, commit)
	if err != nil {
		return err
	}

	fmt.Println("Merging generated PR")
	_, _, err = client.PullRequests.Merge(*repo.Owner.Login, *repo.Name, *pr.Number, title, &github.PullRequestOptions{
		Squash: false,
	})

	fmt.Println("Deleting temporary branch")
	_, deleteErr := client.Git.DeleteRef(*repo.Owner.Login, *repo.Name, fmt.Sprintf("heads/%s", randBranch))
	if deleteErr != nil {
		fmt.Printf("Had a problem trying to delete the temporary branch '%s' after merging it- you should probably delete it manually:  %v", randBranch, deleteErr)
	}

	return err
}
