package main

import (
	"fmt"
	"io/ioutil"
	"os"
	"os/exec"
	"os/user"
	"path/filepath"
	"regexp"
	"runtime"
	"strings"
	"syscall"

	log "github.com/Sirupsen/logrus"
	"github.com/golang/protobuf/proto"
	"github.com/pkg/errors"
	"github.com/urfave/cli"

	"code.justin.tv/dta/rockpaperscissors/client/blueprint"
	"code.justin.tv/dta/rockpaperscissors/client/projectmetadata"
	pb "code.justin.tv/dta/rockpaperscissors/proto"
)

func init() {
	cmd := cli.Command{
		Name:  "enroll",
		Usage: "setup a project for Rock Paper Scissors from a local git repo",
		Flags: []cli.Flag{
			cli.BoolTFlag{
				Name:  "commit-blueprint",
				Usage: "Commit the generated blueprint to the current repository.",
			},
			cli.BoolTFlag{
				Name:  "update-metadata",
				Usage: "Go ahead and immediately update the project metadata in RPS",
			},
			cli.StringFlag{
				Name:  "blueprint",
				Usage: "Use blueprint in `FILE` instead of opening an editor. Implies --commit-blueprint=false",
			},
			cli.BoolFlag{
				Name:  "template",
				Usage: "Instead of normal operation, just print a blueprint template",
			},
		},
		Action: enrollRepositoryAction,
	}
	app.Commands = append(app.Commands, cmd)
}

var (
	orgGuess = map[string]string{
		"dta":      "CAPE",
		"identity": "CAPE",
		"qa":       "CAPE",
		"release":  "CAPE",
		"systems":  "CAPE",
		"Video":    "Broadcaster Success",
	}
	teamGuess = map[string]string{
		"dta":      "DevTools",
		"identity": "Identity",
		"qa":       "QE",
		"release":  "DevTools",
		"revenue":  "Revenue",
		"systems":  "Systems",
		"video":    "Video",
	}
	jiraProjectGuess = map[string]string{
		"dta":      "DTA",
		"edge":     "EDGE",
		"identity": "ID",
		"qa":       "QE",
		"release":  "DTA",
		"revenue":  "REVENUE",
		"sdk":      "SDK",
		"systems":  "SYS",
		"web":      "WEBP",
	}
	// Other GitHub orgs:
	//   twitch
	//   gds
	//   commerce
	//   common

	// Matches either:
	//   git@git-aws.internal.justin.tv:systems/plucker.git
	//   git+ssh://git@git-aws.internal.justin.tv/systems/plucker.git
	//   https://git-aws.internal.justin.tv/dta/rockpaperscissors.git
	gitURLRegex = regexp.MustCompile(
		`^(?:git+ssh|https?://)?(?:[^@]+@)?(?P<host>[^:/]+)[:/](?P<name>[^\.]+)(?:\.git)?$`)
)

func getHostAndNameFromURL(gitURL string) (string, string, error) {
	match := gitURLRegex.FindStringSubmatch(gitURL)
	if match == nil {
		return "", "", nil
	}

	return match[1], match[2], nil
}

func getLocalSourceRepository() (string, string, error) {
	// TODO: support older versions of git that don't have get-url
	out, err := exec.Command(
		"git", "config", "--local", "remote.origin.url").Output()
	if err != nil {
		return "", "", errors.Wrap(err,
			"Error while getting the remote git repository")
	}

	url := strings.Trim(string(out), "\r\n")
	return getHostAndNameFromURL(url)
}

func getEditorProg() []string {
	editor := os.Getenv("VISUAL")
	if editor != "" {
		return []string{editor}
	}

	editor = os.Getenv("EDITOR")
	if editor != "" {
		return []string{editor}
	}

	if runtime.GOOS == "darwin" {
		return []string{"open", "-Wn", "-t"}
	}

	if _, err := os.Stat("/usr/bin/editor"); err == nil {
		return []string{"/usr/bin/editor"}
	}

	return []string{"vi"}
}

func editBlueprint(content string) (string, error) {
	f, err := ioutil.TempFile("", "blueprint")
	if err != nil {
		return "", errors.Wrap(err, "Error making temporary file")
	}
	defer func() {
		err = os.Remove(f.Name())
		if err != nil {
			log.Error(err)
		}
	}()
	defer func() {
		err = f.Close()
		if err != nil {
			log.Error(err)
		}
	}()
	_, err = f.WriteString(content)
	if err != nil {
		return "", errors.Wrap(err, "Error writing to temporary file")
	}

	editor := getEditorProg()
	editor = append(editor, f.Name())

	cmd := exec.Command(editor[0], editor[1:]...)
	cmd.Stdin = os.Stdin
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	err = cmd.Run()
	if err != nil {
		return "", errors.Wrapf(err, "Error running editor (%q)", editor)
	}

	newContentBytes, err := ioutil.ReadFile(f.Name())
	if err != nil {
		return "", errors.Wrap(err, "Error reading temporary file")
	}

	newContent := string(newContentBytes)
	newContent = regexp.MustCompile(`[“”]`).ReplaceAllLiteralString(newContent, "\"")
	return newContent, nil
}

func getProjectMetadataByGitHubRepo(client *projectmetadata.Client, repoHost, repoName string) *pb.ProjectMetadata {
	// TODO: replace this with actual query in API

	projects, err := client.ListProjects(false)
	if err != nil {
		log.Warn(err)
		return nil
	}

	for _, minimalProjectMetadata := range projects {
		metadata, err := client.GetMetadata(minimalProjectMetadata.GetProjectId())
		if err != nil {
			log.Warn(err)
			continue
		}

		for _, sourceRepository := range metadata.GetSourceRepositories() {
			gitHubRepository := sourceRepository.GetGithubRepository()
			if gitHubRepository == nil {
				continue
			}
			if (gitHubRepository.GetHost() == repoHost) && (gitHubRepository.GetName() == repoName) {
				return metadata
			}
		}
	}

	return nil
}

func makeDefaultProjectMetadata(client *projectmetadata.Client) (*pb.ProjectMetadata, error) {
	// TODO: read jenkins.groovy to guess their jenkins jobs

	repoHost, repoName, err := getLocalSourceRepository()
	if err != nil {
		return nil, errors.Wrap(err, "Error getting local repository information")
	}

	metadata := getProjectMetadataByGitHubRepo(client, repoHost, repoName)
	if metadata == nil {
		metadata = &pb.ProjectMetadata{}
	}

	if len(metadata.GetTechLead()) == 0 {
		currentUser, err := user.Current()
		if err != nil {
			return nil, errors.Wrap(err, "Error getting current user")
		}
		metadata.TechLead = []string{currentUser.Username}
	}

	if len(metadata.SourceRepositories) == 0 {
		metadata.SourceRepositories = []*pb.SourceRepository{
			&pb.SourceRepository{
				Repository: &pb.SourceRepository_GithubRepository{
					GithubRepository: &pb.GitHubRepository{
						Host: proto.String(repoHost),
						Name: proto.String(repoName),
					},
				},
			},
		}
	}

	match := regexp.MustCompile(`(.*)/(.*)`).FindStringSubmatch(repoName)
	if match != nil {
		if metadata.GetProjectId() == "" {
			metadata.ProjectId = proto.String(match[2])
		}
		if metadata.GetProjectName() == "" {
			metadata.ProjectName = proto.String(match[2])
		}

		if metadata.GetOrgName() == "" {
			if orgName, ok := orgGuess[match[1]]; ok {
				metadata.OrgName = proto.String(orgName)
			}
		}

		if metadata.GetTeamName() == "" {
			if teamName, ok := teamGuess[match[1]]; ok {
				metadata.TeamName = proto.String(teamName)
			}
		}

		if len(metadata.GetIssueTracker()) == 0 {
			if jiraProject, ok := jiraProjectGuess[match[1]]; ok {
				metadata.IssueTracker = []*pb.IssueTracker{
					&pb.IssueTracker{
						Tracker: &pb.IssueTracker_JiraProject{
							JiraProject: &pb.JiraProject{
								Project: proto.String(jiraProject),
							},
						},
					},
				}
			}
		}
	}

	return metadata, nil
}

func gitCommitFileLocally(filename string, message string) error {
	// TODO: prompt user to ask if it's ok to commit/push and/or edit message.
	// TODO: commit directly or make pull request?

	cleanFilename := filepath.Clean(filename)

	err := exec.Command("git", "add", "--", cleanFilename).Run()
	if err != nil {
		if eerr, ok := err.(*exec.ExitError); ok {
			return errors.Wrapf(err, "Error running git add:\n%s", eerr.Stderr)
		}
		return err
	}

	err = exec.Command(
		"git", "diff-index", "--quiet", "--cached", "--exit-code", "HEAD", "--",
		cleanFilename).Run()
	if err == nil {
		// No changes, don't do anything.
		return nil
	}
	exitError, ok := err.(*exec.ExitError)
	if !ok {
		// Command failed to run at all
		return err
	}
	ws := exitError.Sys().(syscall.WaitStatus)
	if ws.ExitStatus() != 1 {
		// "1" means a file change, which is what we want, anything else is a real error.
		return errors.Wrapf(err, "Error running git diff-index:\n%s", exitError.Stderr)
	}

	err = exec.Command("git", "commit", "-m", message, "--", cleanFilename).Run()
	if err != nil {
		if eerr, ok := err.(*exec.ExitError); ok {
			return errors.Wrapf(err, "Error running git add:\n%s", eerr.Stderr)
		}
		return err
	}

	return nil
}

func enrollRepositoryAction(c *cli.Context) error {
	client, err := projectmetadata.NewClient(c.GlobalString("rps-addr"))
	if err != nil {
		return errors.Wrap(err, "Error making connection to project metadata service")
	}
	defer func() {
		err = client.Close()
		if err != nil {
			log.Error(err)
		}
	}()

	var defaultProjectMetadata *pb.ProjectMetadata
	var content string
	if c.Bool("template") {
		defaultProjectMetadata, err = makeDefaultProjectMetadata(client)
		if err != nil {
			return err
		}
		content, err = blueprint.MakeTemplate(defaultProjectMetadata)
		if err != nil {
			return err
		}
		fmt.Print(content)
		return nil
	}

	var metadata *pb.ProjectMetadata
	if len(c.String("blueprint")) > 0 {
		metadata, err = blueprint.ReadFile(c.String("blueprint"))
		if err != nil {
			return err
		}
	} else {
		defaultProjectMetadata, err = makeDefaultProjectMetadata(client)
		if err != nil {
			return err
		}
		content, err = blueprint.MakeTemplate(defaultProjectMetadata)
		if err != nil {
			return err
		}
		for {
			var newContent string
			newContent, err = editBlueprint(content)
			if err != nil {
				log.Error(err)
				continue
			}
			content = newContent
			metadata, err = blueprint.Parse(content)
			if err != nil {
				log.Error(err)
				continue
			}
			break
		}

		filename := metadata.GetProjectId() + ".blueprint"
		err = ioutil.WriteFile(
			filename, []byte(proto.MarshalTextString(metadata)), 0644)
		if err != nil {
			return errors.Wrapf(err, "Error writing blueprint file (%s)", filename)
		}

		if c.BoolT("commit-blueprint") {
			err = gitCommitFileLocally(
				filename, "Blueprint created with rpstool enroll")
			if err != nil {
				return errors.Wrap(err, "Error committing blueprint file to git")
			}
		}
	}

	if c.BoolT("update-metadata") {
		err = client.UpdateMetadata(metadata)
		if err != nil {
			return errors.Wrap(err, "Error updating project metadata")
		}
	}

	return nil
}
