package github

import (
	"bytes"
	"crypto/sha1"
	"fmt"
	"io"
	"io/ioutil"
	"math/rand"
	"net/url"
	"os"
	"os/exec"
	"os/user"
	"path"
	"strings"
	"text/template"
	"time"

	"code.justin.tv/dta/twitch-create-service/internal/templates"

	"github.com/google/go-github/github"
	"golang.org/x/oauth2"
)

var (
	githubURL = "https://git-aws.internal.justin.tv/api/v3/"
)

type CommitTemplate struct {
	Path              string
	T                 string
	Data              interface{}
	PostTemplatedFunc func([]byte) ([]byte, error)
}

func init() {
	rand.Seed(time.Now().UTC().UnixNano())
}

func GithubClient(tokenFlag string) (*github.Client, error) {
	if tokenFlag != "" {
		return clientFromToken(tokenFlag)
	}

	u, err := user.Current()
	if err != nil {
		return nil, err
	}

	p := path.Join(u.HomeDir, ".twitch-create-service/token")

	if _, err := os.Stat(p); err == nil {
		token, err := ioutil.ReadFile(p)
		if err != nil {
			return nil, err
		}

		return clientFromToken(string(token))
	}

	return nil, fmt.Errorf("unable to find api token")
}

func clientFromToken(token string) (*github.Client, error) {
	token = strings.TrimSpace(token)

	ts := oauth2.StaticTokenSource(
		&oauth2.Token{AccessToken: token},
	)
	tc := oauth2.NewClient(oauth2.NoContext, ts)
	client := github.NewClient(tc)

	base, err := url.Parse(githubURL)
	if err != nil {
		return nil, err
	}

	client.BaseURL = base
	return client, nil
}

func CreateRepository(client *github.Client, name string, deleteRepo bool) (*github.Repository, error) {
	var org, n string

	user, _, err := client.Users.Get("")
	if err != nil {
		return nil, err
	}

	parts := strings.Split(name, "/")
	if len(parts) > 1 {
		org = parts[0]
		n = parts[1]
	} else {
		org = ""
		n = parts[0]
	}

	lookup := *user.Login
	if org != "" {
		lookup = org
	}

	r, _, err := client.Repositories.Get(lookup, n)
	if err == nil {
		if deleteRepo {
			_, err = client.Repositories.Delete(lookup, n)
			if err != nil {
				return nil, fmt.Errorf("error deleting repo %v", err)
			}
		} else {
			// Already created and we weren't asked to delete it, so just return it.
			return r, err
		}
	}

	autoInit := true
	hasIssues := true
	repo := &github.Repository{
		Name:      &n,
		AutoInit:  &autoInit,
		HasIssues: &hasIssues,
	}

	r, _, err = client.Repositories.Create(org, repo)

	return r, err
}

func GetLastestCommit(client *github.Client, repo *github.Repository, branch string) (*github.Reference, *github.Commit, error) {
	ref, _, err := client.Git.GetRef(*repo.Owner.Login, *repo.Name, fmt.Sprintf("heads/%v", branch))
	if err != nil {
		return nil, nil, err
	}

	baseCommit, _, err := client.Git.GetCommit(*repo.Owner.Login, *repo.Name, *ref.Object.SHA)
	if err != nil {
		return nil, nil, err
	}

	return ref, baseCommit, nil
}

func gitSha(objType string, content string) (string, error) {
	h := sha1.New()
	// Git object shas are a SHA1 over "<type> <length>\0<content>"
	// where type is something like "blob" and length is content length as an
	// ascii number.
	_, err := io.WriteString(h, fmt.Sprintf("%s %d\x00", objType, len(content)))
	if err != nil {
		return "", err
	}
	_, err = io.WriteString(h, content)
	if err != nil {
		return "", err
	}
	return fmt.Sprintf("%x", h.Sum(nil)), nil
}

func createCommitFromTemplates(client *github.Client, repo *github.Repository, commitMsg string, baseCommit *github.Commit, templates []CommitTemplate) (*github.Commit, error) {
	commitMsg += "\n\nPrepared via twitch-create-service"

	baseTree, _, err := client.Git.GetTree(*repo.Owner.Login, *repo.Name, *baseCommit.Tree.SHA, true)
	if err != nil {
		return nil, err
	}

	var entries []github.TreeEntry
	for _, t := range templates {
		entry, err := processTemplate(t)
		if err != nil {
			return nil, err
		}

		// compare content of this entry with what's already in repo and only add entry if new/different
		addEntry := true
		for _, baseTreeEntry := range baseTree.Entries {
			if *entry.Path == *baseTreeEntry.Path {
				entrySha, err := gitSha("blob", *entry.Content)
				if err != nil {
					return nil, err
				}
				if entrySha == *baseTreeEntry.SHA {
					addEntry = false
					break
				}
			}
		}

		if addEntry {
			entries = append(entries, *entry)
		}
	}

	if len(entries) == 0 {
		return nil, nil
	}

	tree, _, err := client.Git.CreateTree(*repo.Owner.Login, *repo.Name, *baseCommit.Tree.SHA, entries)
	if err != nil {
		return nil, err
	}

	commit := &github.Commit{
		Message: github.String(commitMsg),
		Tree:    tree,
		Parents: []github.Commit{*baseCommit},
	}
	commit, _, err = client.Git.CreateCommit(*repo.Owner.Login, *repo.Name, commit)
	if err != nil {
		return nil, err
	}

	return commit, nil
}

func FileExists(client *github.Client, repo *github.Repository, path string) bool {
	_, _, _, err := client.Repositories.GetContents(*repo.Owner.Login, *repo.Name, path, nil)
	if err != nil {
		return false
	}

	return true
}

// CommitFilesToBranch updates an existing branch's code.
func CommitFilesToBranch(client *github.Client, repo *github.Repository, branch, commitMsg string, templates []CommitTemplate) error {
	ref, baseCommit, err := GetLastestCommit(client, repo, branch)
	if err != nil {
		return err
	}

	commit, err := createCommitFromTemplates(client, repo, commitMsg, baseCommit, templates)
	if err != nil {
		return err
	}

	if commit != nil {
		ref.Object.SHA = commit.SHA

		_, _, err = client.Git.UpdateRef(*repo.Owner.Login, *repo.Name, ref, false)
		if err != nil {
			return err
		}
	}

	return nil
}

// CommitViaPullRequest creates a new branch, opens a pull request for it, merges that PR, then deletes the branch.
func CommitViaPullRequest(client *github.Client, repo *github.Repository, appName, branch, commitMsg string, templates []CommitTemplate) error {
	refName := fmt.Sprintf("heads/%v", branch)

	_, _, err := client.Git.GetRef(*repo.Owner.Login, *repo.Name, refName)
	if err == nil {
		return fmt.Errorf("ref heads/%v already exists", branch)
	}

	_, baseCommit, err := GetLastestCommit(client, repo, "master")
	if err != nil {
		return err
	}

	commit, err := createCommitFromTemplates(client, repo, commitMsg, baseCommit, templates)
	if err != nil {
		return err
	}

	newRef := &github.Reference{
		Ref: github.String(refName),
		Object: &github.GitObject{
			SHA: commit.SHA,
		},
	}

	_, _, err = client.Git.CreateRef(*repo.Owner.Login, *repo.Name, newRef)
	if err != nil {
		return err
	}

	pr, _, err := client.PullRequests.Create(*repo.Owner.Login, *repo.Name, &github.NewPullRequest{
		Title: github.String(fmt.Sprintf("%v module", appName)),
		Head:  github.String(branch),
		Base:  github.String("master"),
		Body:  github.String("Prepared via twitch-create-service"),
	})
	if err != nil {
		return err
	}

	_, _, err = client.PullRequests.Merge(*repo.Owner.Login, *repo.Name, *pr.Number, fmt.Sprintf("Merging %v module", appName))
	if err != nil {
		return err
	}

	// TODO: find out why GH said the branch delete was reverted in PR ui
	// _, err = client.Git.DeleteRef(*repo.Owner.Login, *repo.Name, refName)
	// if err != nil {
	// 	return err
	// }

	return nil
}

func processTemplate(templateToCommit CommitTemplate) (*github.TreeEntry, error) {
	tmpl, err := template.New(templateToCommit.Path).Parse(templateToCommit.T)
	if err != nil {
		return nil, err
	}

	buf := new(bytes.Buffer)
	err = tmpl.Execute(buf, templateToCommit.Data)
	if err != nil {
		return nil, err
	}

	if templateToCommit.PostTemplatedFunc != nil {
		t := buf.Bytes()
		buf.Reset()
		o, err := templateToCommit.PostTemplatedFunc(t)
		if err != nil {
			return nil, err
		}
		_, err = buf.Write(o)
		if err != nil {
			return nil, err
		}
	}

	return &github.TreeEntry{
		Path:    github.String(templateToCommit.Path),
		Content: github.String(buf.String()),
		Mode:    github.String("100644"),
	}, nil
}

func CloneLocally(user, token string, checkoutBaseDir string, repo *github.Repository, verbose bool) (string, error) {
	checkoutPath := fmt.Sprintf("%v/go/src/code.justin.tv/%v/%v", checkoutBaseDir, *repo.Owner.Login, *repo.Name)

	_, err := os.Stat(checkoutPath)
	if !os.IsNotExist(err) {
		return "", fmt.Errorf("%v already exists", checkoutPath)
	}

	err = os.MkdirAll(checkoutPath, 0755)
	if err != nil {
		return "", err
	}

	cmd := exec.Command("git", "clone", fmt.Sprintf("https://%v:%v@git-aws.internal.justin.tv/%v/%v.git", user, token, *repo.Owner.Login, *repo.Name), ".")
	cmd.Dir = checkoutPath
	if verbose {
		cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr
	}
	err = cmd.Run()
	if err != nil {
		return "", err
	}

	return checkoutPath, nil
}

type TemplateToCommit struct {
	TemplatePath      string
	RepoPath          string
	PostTemplatedFunc func([]byte) ([]byte, error)
}

func PrepareTemplatesForCommit(files []TemplateToCommit, data interface{}) ([]CommitTemplate, error) {
	var commit []CommitTemplate

	for _, t := range files {
		templateData, err := templates.Asset(t.TemplatePath)
		if err != nil {
			return nil, err
		}

		commit = append(commit, CommitTemplate{
			Path:              t.RepoPath,
			T:                 string(templateData),
			Data:              data,
			PostTemplatedFunc: t.PostTemplatedFunc,
		})
	}

	return commit, nil
}
