package trivy

import (
	"net/url"
	"os"
	"path/filepath"
	"strings"

	git "github.com/go-git/go-git/v5"
	"github.com/go-git/go-git/v5/config"
	"github.com/go-git/go-git/v5/plumbing"
	"github.com/go-git/go-git/v5/plumbing/transport/http"
	"github.com/pingcap-inc/security/baseline/pkg/utils"
	"golang.org/x/xerrors"
)

func githubAuth(token string) *http.BasicAuth {
	return &http.BasicAuth{
		Username: "PingCAP-Security-baseline",
		Password: token,
	}
}

func checkoutToDefaultBranch(repo *git.Repository) error {
	if _, err := repo.Branch("main"); err == nil {
		return checkoutRepo(repo, "branch@main")
	}

	if _, err := repo.Branch("master"); err == nil {
		return checkoutRepo(repo, "branch@master")
	}

	return xerrors.Errorf("Failed to checkout to default branch, which is neither `main` nor `master`.")
}

func checkoutBranch(repo *git.Repository, wt *git.Worktree, branchName string) error {
	if branch, err := repo.Branch(branchName); err != nil {
		// refs/heads/<localBranchName>
		localBranchReferenceName := plumbing.NewBranchReferenceName(branchName)
		// refs/remotes/origin/<remoteBranchName>
		remoteReferenceName := plumbing.NewRemoteReferenceName("origin", branchName)

		if err := repo.CreateBranch(&config.Branch{Name: branchName, Remote: "origin", Merge: localBranchReferenceName}); err != nil {
			return err
		} else {
			return repo.Storer.SetReference(plumbing.NewSymbolicReference(localBranchReferenceName, remoteReferenceName))
		}
	} else {
		return wt.Checkout(&git.CheckoutOptions{
			Branch: branch.Merge,
			Force:  true,
		})
	}
}

func checkoutRepo(repo *git.Repository, refString string) error {
	wt, err := repo.Worktree()
	if err != nil {
		return xerrors.Errorf("Checkout to reference `%s` failed, %+v", refString, err)
	}

	var refType, refName string

	if ref := strings.Split(refString, "@"); len(ref) == 2 {
		refType, refName = ref[0], ref[1]
	} else {
		return xerrors.Errorf("Malformed reference field `%s`, [branch|tag|commit]@[refName]", refString)
	}

	switch refType {
	case "branch":
		return checkoutBranch(repo, wt, refName)
	case "tag":
		if tag, err := repo.Tag(refName); err == nil {
			return wt.Checkout(&git.CheckoutOptions{
				Branch: tag.Name(),
				Force:  true,
			})
		} else {
			return xerrors.Errorf("Checkout to tag `%s` failed, %+v", refName, err)
		}
	case "commit":
		return wt.Checkout(&git.CheckoutOptions{
			Hash:  plumbing.NewHash(refName),
			Force: true,
		})
	default:
		return xerrors.Errorf("checkout failed: unknown reference type `%s`, should be `branch|tag|commit`", refType)
	}
}

func cloneRepo(GITHUB_TOKEN string, repoURL *url.URL, repoDir string) (*git.Repository, error) {
	if _, err := os.Stat(repoDir); os.IsNotExist(err) {
		err = os.MkdirAll(repoDir, 0o700)
		if err != nil {
			return nil, xerrors.Errorf("Clone repo `%s` to `%s` failed, %+v", repoURL.String(), repoDir, err)
		}
	}

	repo, err := git.PlainClone(repoDir, false, &git.CloneOptions{
		URL:      repoURL.String(),
		Auth:     githubAuth(GITHUB_TOKEN),
		Progress: os.Stdout,
	})

	if err != nil {
		return nil, xerrors.Errorf("Clone repo `%s` to `%s` failed, %+v", repoURL.String(), repoDir, err)
	} else {
		return repo, err
	}
}

func openRepo(repoDir string) (*git.Repository, error) {
	repo, err := git.PlainOpen(repoDir)
	if err != nil {
		return nil, xerrors.Errorf("Open repo from `%s` failed, %+v", repoDir, err)
	} else {
		return repo, err
	}
}

func pullRepo(repo *git.Repository, GITHUB_TOKEN string) error {
	wt, err := repo.Worktree()
	if err != nil {
		return xerrors.Errorf("Pull repo failed, %+v", err)
	}

	pullOptions := git.PullOptions{
		Auth:     githubAuth(GITHUB_TOKEN),
		Progress: os.Stdout,
	}

	err = wt.Pull(&pullOptions)
	if err != nil {
		if !xerrors.Is(err, git.NoErrAlreadyUpToDate) {
			return xerrors.Errorf("Pull repo failed, %+v", err)
		}
	}

	return nil
}

func LockRepo(GITHUB_TOKEN string, cacheDir string, target string) (*utils.DirLock, *url.URL, error) {
	_, repoURL, err := utils.ParseRepoUrl(target)
	if err != nil {
		return nil, nil, err
	}

	repoDir := filepath.Join(filepath.Join(cacheDir, "git"), repoURL.Host, repoURL.Path)

	if _, err := os.Stat(repoDir); os.IsNotExist(err) {
		if err := os.MkdirAll(repoDir, 0o700); err != nil {
			return nil, nil, err
		}
	}

	lock := utils.NewDirLock(repoDir)
	if err := lock.Lock(); err != nil {
		return nil, nil, err
	} else {
		return lock, repoURL, nil
	}
}

func CloneRepo(GITHUB_TOKEN string, cacheDir string, repoURL *url.URL, repoDir string) error {
	if _, err := os.Stat(repoDir); os.IsNotExist(err) {
		return err
	} else {
		if _, err := os.Stat(filepath.Join(repoDir, ".git")); os.IsNotExist(err) {
			_, err = cloneRepo(GITHUB_TOKEN, repoURL, repoDir)
			return err
		}

		return err
	}
}

func UpdateRepo(GITHUB_TOKEN string, cacheDir string, repoURL *url.URL, repoDir string) error {
	refString := repoURL.Fragment

	var repo *git.Repository

	if _, err := os.Stat(repoDir); os.IsNotExist(err) {
		return err
	} else {
		if repo, err = openRepo(repoDir); err != nil {
			return err
		}

		if err := checkoutToDefaultBranch(repo); err != nil {
			return err
		}

		if err := pullRepo(repo, GITHUB_TOKEN); err != nil {
			return err
		}

		if err := checkoutRepo(repo, refString); err != nil {
			return err
		}
	}

	return nil
}
