package main

import (
	"fmt"
	"net/url"
	"os"
	"path"
	"path/filepath"
	"strings"

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

var (
	buildCommit string
)

func main() {
	fmt.Printf("Drone GitHub Release Plugin built from %s\n", buildCommit)

	workspace := drone.Workspace{}
	repo := drone.Repo{}
	build := drone.Build{}
	vargs := Params{}

	plugin.Param("workspace", &workspace)
	plugin.Param("repo", &repo)
	plugin.Param("build", &build)
	plugin.Param("vargs", &vargs)
	plugin.MustParse()

	if build.Event != "tag" {
		fmt.Printf("The GitHub Release plugin is only available for tags\n")
		os.Exit(0)
	}

	if vargs.FileExists == "" {
		vargs.FileExists = "overwrite"
	}
	if !fileExistsValues[vargs.FileExists] {
		fmt.Printf("invalid value for file_exists: use [empty], overwrite, skip or fail")
	}

	if vargs.BaseURL == "" {
		vargs.BaseURL = "https://api.github.com/"
	} else if !strings.HasSuffix(vargs.BaseURL, "/") {
		vargs.BaseURL = vargs.BaseURL + "/"
	}

	if vargs.UploadURL == "" {
		vargs.UploadURL = "https://uploads.github.com/"
	} else if !strings.HasSuffix(vargs.UploadURL, "/") {
		vargs.UploadURL = vargs.UploadURL + "/"
	}

	if vargs.APIKey == "" {
		fmt.Printf("You must provide an API key\n")
		os.Exit(1)
	}

	if workspace.Path != "" {
		os.Chdir(workspace.Path)
	}

	var files []string
	for _, glob := range vargs.Files.Slice() {
		globed, err := filepath.Glob(glob)
		if err != nil {
			fmt.Printf("Failed to glob %s\n", glob)
			os.Exit(1)
		}
		if globed != nil {
			files = append(files, globed...)
		}
	}

	if vargs.Checksum.Len() > 0 {
		var err error
		files, err = writeChecksums(files, vargs.Checksum.Slice())
		if err != nil {
			fmt.Println(err)
			os.Exit(1)
		}
	}

	baseURL, err := url.Parse(vargs.BaseURL)
	if err != nil {
		fmt.Printf("Failed to parse base URL\n")
		os.Exit(1)
	}

	uploadURL, err := url.Parse(vargs.UploadURL)
	if err != nil {
		fmt.Printf("Failed to parse upload URL\n")
		os.Exit(1)
	}

	ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: vargs.APIKey})
	tc := oauth2.NewClient(oauth2.NoContext, ts)

	client := github.NewClient(tc)
	client.BaseURL = baseURL
	client.UploadURL = uploadURL

	rc := releaseClient{
		Client: client,
		Owner:  repo.Owner,
		Repo:   repo.Name,
		Tag:    filepath.Base(build.Ref),
		Draft:  vargs.Draft,
		FileExists: vargs.FileExists,
	}

	release, err := rc.buildRelease()
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}

	if err := rc.uploadFiles(*release.ID, files); err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
}

var fileExistsValues = map[string]bool{
	"overwrite": true,
	"fail":      true,
	"skip":      true,
}

// Release holds ties the drone env data and github client together.
type releaseClient struct {
	*github.Client
	Owner      string
	Repo       string
	Tag        string
	Draft      bool
	FileExists string
}

func (rc *releaseClient) buildRelease() (*github.RepositoryRelease, error) {

	// first attempt to get a release by that tag
	release, err := rc.getRelease()
	if err != nil && release == nil {
		fmt.Println(err)
	} else if release != nil {
		return release, nil
	}

	// if no release was found by that tag, create a new one
	release, err = rc.newRelease()
	if err != nil {
		return nil, fmt.Errorf("Failed to retrieve or create a release: %s", err)
	}

	return release, nil
}

func (rc *releaseClient) getRelease() (*github.RepositoryRelease, error) {
	release, _, err := rc.Client.Repositories.GetReleaseByTag(rc.Owner, rc.Repo, rc.Tag)
	if err != nil {
		return nil, fmt.Errorf("Release %s not found", rc.Tag)
	}

	fmt.Printf("Successfully retrieved %s release\n", rc.Tag)
	return release, nil
}

func (rc *releaseClient) newRelease() (*github.RepositoryRelease, error) {
	rr := &github.RepositoryRelease{
		TagName: github.String(rc.Tag),
		Draft:   &rc.Draft,
	}
	release, _, err := rc.Client.Repositories.CreateRelease(rc.Owner, rc.Repo, rr)
	if err != nil {
		return nil, fmt.Errorf("Failed to create release: %s", err)
	}

	fmt.Printf("Successfully created %s release\n", rc.Tag)
	return release, nil
}

func (rc *releaseClient) uploadFiles(id int, files []string) error {
	assets, _, err := rc.Client.Repositories.ListReleaseAssets(rc.Owner, rc.Repo, id, &github.ListOptions{})
	if err != nil {
		return fmt.Errorf("Failed to fetch existing assets: %s", err)
	}

	var uploadFiles []string
files:
	for _, file := range files {
		for _, asset := range assets {
			if *asset.Name == path.Base(file) {
				switch rc.FileExists {
				case "overwrite":
					// do nothing
				case "fail":
					return fmt.Errorf("Asset file %s already exists", path.Base(file))
				case "skip":
					fmt.Printf("Skipping pre-existing %s artifact\n", *asset.Name)
					continue files
				default:
					return fmt.Errorf("Internal error, unkown file_exist value %s", rc.FileExists)
				}
			}
		}
		uploadFiles = append(uploadFiles, file)
	}

	for _, file := range uploadFiles {
		handle, err := os.Open(file)
		if err != nil {
			return fmt.Errorf("Failed to read %s artifact: %s", file, err)
		}

		for _, asset := range assets {
			if *asset.Name == path.Base(file) {
				if _, err := rc.Client.Repositories.DeleteReleaseAsset(rc.Owner, rc.Repo, *asset.ID); err != nil {
					return fmt.Errorf("Failed to delete %s artifact: %s", file, err)
				}
				fmt.Printf("Successfully deleted old %s artifact\n", *asset.Name)
			}
		}

		uo := &github.UploadOptions{Name: path.Base(file)}
		if _, _, err = rc.Client.Repositories.UploadReleaseAsset(rc.Owner, rc.Repo, id, uo, handle); err != nil {
			return fmt.Errorf("Failed to upload %s artifact: %s", file, err)
		}

		fmt.Printf("Successfully uploaded %s artifact\n", file)
	}

	return nil
}

func writeChecksums(files, methods []string) ([]string, error) {

	checksums := make(map[string][]string)
	for _, method := range methods {
		for _, file := range files {
			handle, err := os.Open(file)
			if err != nil {
				return nil, fmt.Errorf("Failed to read %s artifact: %s", file, err)
			}

			hash, err := checksum(handle, method)
			if err != nil {
				return nil, err
			}

			checksums[method] = append(checksums[method], hash, file)
		}
	}

	for method, results := range checksums {
		filename := method + "sum.txt"
		f, err := os.Create(filename)
		if err != nil {
			return nil, err
		}

		for i := 0; i < len(results); i += 2 {
			hash := results[i]
			file := results[i+1]
			if _, err := f.WriteString(fmt.Sprintf("%s  %s\n", hash, file)); err != nil {
				return nil, err
			}
		}
		files = append(files, filename)
	}
	return files, nil
}