package github

import (
	"fmt"
	"net/http"
	"net/url"
	"strconv"
	"strings"
	"time"

	"github.com/drone/drone/plugin/remote/github/oauth"
	"github.com/drone/drone/shared/httputil"
	"github.com/drone/drone/shared/model"
	"github.com/drone/go-github/github"
)

const (
	DefaultAPI   = "https://api.github.com/"
	DefaultURL   = "https://github.com"
	DefaultScope = "repo,repo:status,user:email"
)

type GitHub struct {
	URL        string
	API        string
	Client     string
	Secret     string
	Private    bool
	SkipVerify bool
}

func New(url, api, client, secret string, private, skipVerify bool) *GitHub {
	var github = GitHub{
		URL:        url,
		API:        api,
		Client:     client,
		Secret:     secret,
		Private:    private,
		SkipVerify: skipVerify,
	}
	// the API must have a trailing slash
	if !strings.HasSuffix(github.API, "/") {
		github.API += "/"
	}
	// the URL must NOT have a trailing slash
	if strings.HasSuffix(github.URL, "/") {
		github.URL = github.URL[:len(github.URL)-1]
	}
	return &github
}

func NewDefault(client, secret string) *GitHub {
	return New(DefaultURL, DefaultAPI, client, secret, false, false)
}

// Authorize handles GitHub API Authorization.
func (r *GitHub) Authorize(res http.ResponseWriter, req *http.Request) (*model.Login, error) {
	var config = &oauth.Config{
		ClientId:     r.Client,
		ClientSecret: r.Secret,
		Scope:        DefaultScope,
		AuthURL:      fmt.Sprintf("%s/login/oauth/authorize", r.URL),
		TokenURL:     fmt.Sprintf("%s/login/oauth/access_token", r.URL),
		RedirectURL:  fmt.Sprintf("%s/api/auth/%s", httputil.GetURL(req), r.GetKind()),
	}

	// get the OAuth code
	var code = req.FormValue("code")
	var state = req.FormValue("state")
	if len(code) == 0 {
		var random = GetRandom()
		httputil.SetCookie(res, req, "github_state", random)
		http.Redirect(res, req, config.AuthCodeURL(random), http.StatusSeeOther)
		return nil, nil
	}

	cookieState := httputil.GetCookie(req, "github_state")
	httputil.DelCookie(res, req, "github_state")
	if cookieState != state {
		return nil, fmt.Errorf("Error matching state in OAuth2 redirect")
	}

	var trans = &oauth.Transport{Config: config}
	var token, err = trans.Exchange(code)
	if err != nil {
		return nil, fmt.Errorf("Error exchanging token. %s", err)
	}

	var client = NewClient(r.API, token.AccessToken, r.SkipVerify)
	var useremail, errr = GetUserEmail(client)
	if errr != nil {
		return nil, fmt.Errorf("Error retrieving user or verified email. %s", errr)
	}

	var login = new(model.Login)
	login.ID = int64(*useremail.ID)
	login.Access = token.AccessToken
	login.Login = *useremail.Login
	login.Email = *useremail.Email
	if useremail.Name != nil {
		login.Name = *useremail.Name
	}

	return login, nil
}

// GetKind returns the internal identifier of this remote GitHub instane.
func (r *GitHub) GetKind() string {
	if r.IsEnterprise() {
		return model.RemoteGithubEnterprise
	} else {
		return model.RemoteGithub
	}
}

// GetHost returns the hostname of this remote GitHub instance.
func (r *GitHub) GetHost() string {
	uri, _ := url.Parse(r.URL)
	return uri.Host
}

// IsEnterprise returns true if the remote system is an
// instance of GitHub Enterprise Edition.
func (r *GitHub) IsEnterprise() bool {
	return r.URL != DefaultURL
}

// GetRepos fetches all repositories that the specified
// user has access to in the remote system.
func (r *GitHub) GetRepos(user *model.User) ([]*model.Repo, error) {
	var repos []*model.Repo
	var client = NewClient(r.API, user.Access, r.SkipVerify)
	var list, err = GetAllRepos(client)
	if err != nil {
		return nil, err
	}

	var remote = r.GetKind()
	var hostname = r.GetHost()

	for _, item := range list {
		var repo = model.Repo{
			UserID:   user.ID,
			Remote:   remote,
			Host:     hostname,
			Owner:    *item.Owner.Login,
			Name:     *item.Name,
			Private:  *item.Private,
			URL:      *item.HTMLURL,
			CloneURL: *item.GitURL,
			GitURL:   *item.GitURL,
			SSHURL:   *item.SSHURL,
			Role:     &model.Perm{},
		}

		if r.Private || repo.Private {
			repo.CloneURL = *item.SSHURL
		}

		// if no permissions we should skip the repository
		// entirely, since this should never happen
		if item.Permissions == nil {
			continue
		}

		repo.Role.Admin = (*item.Permissions)["admin"]
		repo.Role.Write = (*item.Permissions)["push"]
		repo.Role.Read = (*item.Permissions)["pull"]
		repos = append(repos, &repo)
	}

	return repos, err
}

// GetScript fetches the build script (.drone.yml) from the remote
// repository and returns in string format.
func (r *GitHub) GetScript(user *model.User, repo *model.Repo, hook *model.Hook) ([]byte, error) {
	var client = NewClient(r.API, user.Access, r.SkipVerify)
	return GetFile(client, repo.Owner, repo.Name, ".drone.yml", hook.Sha)
}

// Activate activates a repository by adding a Post-commit hook and
// a Public Deploy key, if applicable.
func (r *GitHub) Activate(user *model.User, repo *model.Repo, link string) error {
	var client = NewClient(r.API, user.Access, r.SkipVerify)
	var title, err = GetKeyTitle(link)
	if err != nil {
		return err
	}

	// if the CloneURL is using the SSHURL then we know that
	// we need to add an SSH key to GitHub.
	if repo.SSHURL == repo.CloneURL {
		_, err = CreateUpdateKey(client, repo.Owner, repo.Name, title, repo.PublicKey)
		if err != nil {
			return err
		}
	}

	_, err = CreateUpdateHook(client, repo.Owner, repo.Name, link)
	return err
}

// ParseHook parses the post-commit hook from the Request body
// and returns the required data in a standard format.
func (r *GitHub) ParseHook(req *http.Request) (*model.Hook, error) {
	// handle github ping
	if req.Header.Get("X-Github-Event") == "ping" {
		return nil, nil
	}

	// handle github pull request hook differently
	if req.Header.Get("X-Github-Event") == "pull_request" {
		return r.ParsePullRequestHook(req)
	}

	// parse the github Hook payload
	var payload = GetPayload(req)
	var data, err = github.ParseHook(payload)
	if err != nil {
		return nil, nil
	}

	// make sure this is being triggered because of a commit
	// and not something like a tag deletion or whatever
	if data.IsTag() ||
		data.IsGithubPages() ||
		data.IsHead() == false ||
		data.IsDeleted() {
		return nil, nil
	}

	var hook = new(model.Hook)
	hook.Repo = data.Repo.Name
	hook.Owner = data.Repo.Owner.Login
	hook.Sha = data.Head.Id
	hook.Branch = data.Branch()

	if len(hook.Owner) == 0 {
		hook.Owner = data.Repo.Owner.Name
	}

	// extract the author and message from the commit
	// this is kind of experimental, since I don't know
	// what I'm doing here.
	if data.Head != nil && data.Head.Author != nil {
		hook.Message = data.Head.Message
		hook.Timestamp = data.Head.Timestamp
		hook.Author = data.Head.Author.Email
	} else if data.Commits != nil && len(data.Commits) > 0 && data.Commits[0].Author != nil {
		hook.Message = data.Commits[0].Message
		hook.Timestamp = data.Commits[0].Timestamp
		hook.Author = data.Commits[0].Author.Email
	}

	return hook, nil
}

// ParsePullRequestHook parses the pull request hook from the Request body
// and returns the required data in a standard format.
func (r *GitHub) ParsePullRequestHook(req *http.Request) (*model.Hook, error) {

	// parse the payload to retrieve the pull-request
	// hook meta-data.
	var payload = GetPayload(req)
	var data, err = github.ParsePullRequestHook(payload)
	if err != nil {
		return nil, err
	}

	// ignore these
	if data.Action != "opened" && data.Action != "synchronize" {
		return nil, nil
	}

	// TODO we should also store the pull request branch (ie from x to y)
	//      we can find it here: data.PullRequest.Head.Ref
	var hook = model.Hook{
		Owner:       data.Repo.Owner.Login,
		Repo:        data.Repo.Name,
		Sha:         data.PullRequest.Head.Sha,
		Branch:      data.PullRequest.Head.Ref,
		Author:      data.PullRequest.User.Login,
		Gravatar:    data.PullRequest.User.GravatarId,
		Timestamp:   time.Now().UTC().String(),
		Message:     data.PullRequest.Title,
		PullRequest: strconv.Itoa(data.Number),
	}

	if len(hook.Owner) == 0 {
		hook.Owner = data.Repo.Owner.Name
	}

	return &hook, nil
}