package ghutil

import (
	"crypto/sha1"
	"errors"
	"fmt"
	"io"

	"github.com/google/go-github/github"
)

func ForceCommitToBranch(client *github.Client, repo *github.Repository, branch string, message string, cleanTree bool, entries *[]github.TreeEntry) error {

	baseRef := ""

	//Grab the branch
	ref, _, err := client.Git.GetRef(*repo.Owner.Login, *repo.Name, fmt.Sprintf("heads/%s", branch))
	if err != nil {
		return err
	}

	//Get head commit on the branch
	baseCommit, _, err := client.Git.GetCommit(*repo.Owner.Login, *repo.Name, *ref.Object.SHA)
	if err != nil {
		return err
	}

	//If the commit entries are building on the branch, grab the ref SHA
	if !cleanTree {
		baseRef = *ref.Object.SHA
	}

	//Prep the commit tree
	tree, _, err := client.Git.CreateTree(*repo.Owner.Login, *repo.Name, baseRef, *entries)
	if err != nil {
		return err
	}

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

	ref.Object.SHA = newCommit.SHA
	_, _, err = client.Git.UpdateRef(*repo.Owner.Login, *repo.Name, ref, false)
	return err
}

func CreatePullRequest(client *github.Client, repo *github.Repository, newBranch string, baseBranch string, title string, message string, commit *github.Commit) (*github.PullRequest, error) {
	if commit == nil {
		return nil, errors.New("CreatePullRequest: Commit is nil.")
	}

	//Try to get the commit if it already exists (nil SHA means we just made it up)
	//If it doesn't exist, create it
	var err error
	if commit.SHA != nil {
		commit, _, err = client.Git.GetCommit(*repo.Owner.Login, *repo.Name, *commit.SHA)
	}
	if err != nil || commit.SHA == nil {
		commit, _, err = client.Git.CreateCommit(*repo.Owner.Login, *repo.Name, commit)
		if err != nil {
			return nil, err
		}
	}

	//Branch shouldn't exist already- we don't want to mangle the work of a ree-al human being
	_, _, err = client.Git.GetRef(*repo.Owner.Login, *repo.Name, fmt.Sprintf("heads/%s", newBranch))
	if err == nil {
		return nil, fmt.Errorf("The branch %s already exists", newBranch)
	}

	//Create branch & then PR
	_, _, err = client.Git.CreateRef(*repo.Owner.Login, *repo.Name, &github.Reference{
		Ref: github.String(fmt.Sprintf("heads/%s", newBranch)),
		Object: &github.GitObject{
			SHA: commit.SHA,
		},
	})

	if err != nil {
		return nil, err
	}

	pr, _, err := client.PullRequests.Create(*repo.Owner.Login, *repo.Name, &github.NewPullRequest{
		Title: github.String(title),
		Body:  github.String(message),
		Head:  github.String(newBranch),
		Base:  github.String(baseBranch),
	})
	return pr, err
}

func GetBranchData(client *github.Client, repo *github.Repository, branch string) (string, map[string]github.TreeEntry, error) {
	data := make(map[string]github.TreeEntry)

	//Get the tree for the latest commit on the branch
	ref, _, err := client.Git.GetRef(*repo.Owner.Login, *repo.Name, fmt.Sprintf("heads/%s", branch))
	if err != nil {
		return "", nil, err
	}

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

	for _, entry := range tree.Entries {
		if entry.Path != nil {
			data[*entry.Path] = entry
		}
	}

	return *ref.Object.SHA, data, nil
}

func CreateCommitFromChanges(client *github.Client, repo *github.Repository, branch string, changes map[string]*github.TreeEntry, message string) (*github.Commit, error) {
	branchSha, currentTree, err := GetBranchData(client, repo, branch)
	if err != nil {
		return nil, err
	}

	deletedEntries := false
	var changedEntries []github.TreeEntry
	for path, entry := range changes {
		if entry == nil {
			delete(currentTree, path)
			deletedEntries = true
		} else {
			oldEntry, ok := currentTree[path]
			if ok {
				ok, err = areEntriesSame(oldEntry, *entry)
				if err != nil {
					return nil, err
				}
			}
			if !ok {
				changedEntries = append(changedEntries, *entry)
			}
		}
	}

	//No changes? Let's bail
	if !deletedEntries && len(changedEntries) == 0 {
		return nil, nil
	}

	//Build the new tree
	var parents []github.Commit
	var newTreeEntries []github.TreeEntry
	newTreeEntries = append(newTreeEntries, changedEntries...)

	if deletedEntries {
		//You can't delete an entry when you're building a tree from a base tree,
		//so we have to build a fresh tree from scratch
		for _, entry := range currentTree {
			newTreeEntries = append(newTreeEntries, entry)
		}
	} else {
		//Get the commit
		baseCommit, _, err := client.Git.GetCommit(*repo.Owner.Login, *repo.Name, branchSha)
		if err != nil {
			return nil, err
		}
		parents = []github.Commit{*baseCommit}
	}

	newTree, _, err := client.Git.CreateTree(*repo.Owner.Login, *repo.Name, branchSha, newTreeEntries)
	if err != nil {
		return nil, err
	}

	commit := &github.Commit{
		Message: github.String(message),
		Tree:    newTree,
		Parents: parents,
	}

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

	return commit, nil
}

func areEntriesSame(oldEntry github.TreeEntry, newEntry github.TreeEntry) (bool, error) {
	if *oldEntry.Type != *newEntry.Type || *oldEntry.Path != *newEntry.Path || *oldEntry.Mode != *newEntry.Mode {
		return false, nil
	}

	if *newEntry.Type != "blob" {
		return true, nil
	}

	hash, err := hashEntry(newEntry)
	if err != nil {
		return false, err
	}

	if hash != *oldEntry.SHA {
		return false, nil
	}

	return true, nil
}

func hashEntry(entry github.TreeEntry) (string, error) {
	objType := ""
	if entry.Type != nil {
		objType = *entry.Type
	}

	content := ""
	if entry.Content != nil {
		content = *entry.Content
	}

	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
}
