package repo

import (
	"fmt"
	"net/http"
	"strings"

	access_model "code.gitea.io/gitea/models/perm/access"
	repo_model "code.gitea.io/gitea/models/repo"
	system_model "code.gitea.io/gitea/models/system"
	unit_model "code.gitea.io/gitea/models/unit"
	user_model "code.gitea.io/gitea/models/user"
	"code.gitea.io/gitea/modules/context"
	"code.gitea.io/gitea/modules/git"
	gitea_git "code.gitea.io/gitea/modules/git"
	"code.gitea.io/gitea/modules/log"
	"code.gitea.io/gitea/modules/setting"
	"code.gitea.io/gitea/services/gitdiff"
	hat_activities_models "code.gitlink.org.cn/Gitlink/gitea_hat.git/models/activities"
)

func PrepareComapreDiff(
	ctx *context.APIContext,
	headUser *user_model.User,
	headRepo *repo_model.Repository,
	headGitRepo *gitea_git.Repository,
	compareInfo *gitea_git.CompareInfo,
	baseBranch, headBranch string) bool {
	var (
		err error
	)

	ctx.Data["CommitRepoLink"] = headRepo.Link()

	headCommitID := headBranch
	if ctx.Data["HeadIsCommit"] == false {
		if ctx.Data["HeadIsTag"] == true {
			headCommitID, err = headGitRepo.GetTagCommitID(headBranch)
		} else {
			headCommitID, err = headGitRepo.GetBranchCommitID(headBranch)
		}
		if err != nil {
			ctx.Error(http.StatusInternalServerError, "GetRefCommitID", err)
			return false
		}
	}

	ctx.Data["AfterCommitID"] = headCommitID

	if headCommitID == compareInfo.MergeBase {
		ctx.Data["IsNothingToCompare"] = true
		return true
	}

	repoPath := repo_model.RepoPath(headUser.Name, headRepo.Name)

	gitRepo, _ := gitea_git.OpenRepository(ctx, repoPath)
	defer gitRepo.Close()

	diff, err := gitdiff.GetDiff(gitRepo, &gitdiff.DiffOptions{
		BeforeCommitID:    compareInfo.MergeBase,
		AfterCommitID:     headCommitID,
		MaxLines:          setting.Git.MaxGitDiffLines,
		MaxLineCharacters: setting.Git.MaxGitDiffLineCharacters,
		MaxFiles:          setting.Git.MaxGitDiffFiles,
	})

	if err != nil {
		ctx.Error(http.StatusInternalServerError, "GetDiff", err)
		return false
	}

	if diff.NumFiles == 0 {
		ctx.Data["Diff"] = nil
		ctx.Data["DiffNotAvailable"] = true
	} else {
		ctx.Data["Diff"] = diff
		ctx.Data["DiffNotAvailable"] = false
	}

	return false
}

type CompareCommit struct {
	*git.Commit
	Sha        string
	ParentShas []string
}

func CompareDiff(ctx *context.APIContext) {
	headUser, headRepo, headGitRepo, compareInfo, baseBranch, headBranch := ParseCompareInfo(ctx)
	if ctx.Written() {
		return
	}
	defer headGitRepo.Close()

	_ = PrepareComapreDiff(ctx, headUser, headRepo, headGitRepo, compareInfo, baseBranch, headBranch)

	result := make([]CompareCommit, 0)
	for _, commit := range compareInfo.Commits {
		compareCommit := CompareCommit{
			Commit: commit,
			Sha:    commit.ID.String(),
		}
		for _, p := range commit.Parents {
			compareCommit.ParentShas = append(compareCommit.ParentShas, p.String())
		}
		result = append(result, compareCommit)
	}

	different := struct {
		Commits      []CompareCommit
		Diff         interface{}
		CommitsCount int
		Latestsha    string
	}{
		Commits: result,
		Diff:    ctx.Context.Data["Diff"],
	}

	different.CommitsCount = len(compareInfo.Commits)
	different.Latestsha = compareInfo.HeadCommitID

	ctx.JSON(http.StatusOK, different)
}

func ParseCompareInfo(ctx *context.APIContext) (*user_model.User, *repo_model.Repository, *gitea_git.Repository, *gitea_git.CompareInfo, string, string) {
	baseRepo := ctx.Repo.Repository

	var (
		headUser   *user_model.User
		headRepo   *repo_model.Repository
		headBranch string
		isSameRepo bool
		infoPath   string
		err        error
	)

	infoPath = ctx.Params("*")
	infos := strings.SplitN(infoPath, "...", 2)
	if len(infos) != 2 {
		log.Trace("ParseCompareInfo[%d]: not enough compared branches information %s", baseRepo.ID, infos)
		ctx.NotFound("CompareAndPullRequest", nil)
		return nil, nil, nil, nil, "", ""
	}

	ctx.Data["BaseName"] = baseRepo.OwnerName
	baseBranch := infos[0]
	ctx.Data["BaseBranch"] = baseBranch

	headInfos := strings.Split(infos[1], ":")
	if len(headInfos) == 1 {
		isSameRepo = true
		headUser = ctx.Repo.Owner
		headBranch = headInfos[0]
	} else if len(headInfos) == 2 {
		headInfosSplit := strings.Split(headInfos[0], "/")
		if len(headInfosSplit) == 1 {
			headUser, err = user_model.GetUserByName(ctx, headInfos[0])
			if err != nil {
				if user_model.IsErrUserNotExist(err) {
					ctx.NotFound("GetUserByName", nil)
				} else {
					ctx.Error(http.StatusInternalServerError, "GetUserByName", err)
				}
				return nil, nil, nil, nil, "", ""
			}

			headBranch = headInfos[1]
			isSameRepo = headUser.ID == ctx.Repo.Owner.ID

			if isSameRepo {
				headRepo = baseRepo
			}
		} else {
			headRepo, err = repo_model.GetRepositoryByOwnerAndName(headInfosSplit[0], headInfosSplit[1])
			if err != nil {
				if repo_model.IsErrRepoNotExist(err) {
					ctx.NotFound("GetRepositoryByOwnerAndName", nil)
				} else {
					ctx.Error(http.StatusInternalServerError, "GetRepositoryByOwnerAndName", err)
				}
				return nil, nil, nil, nil, "", ""
			}
			if err := headRepo.GetOwner(ctx); err != nil {
				if user_model.IsErrUserNotExist(err) {
					ctx.NotFound("GetOwner", nil)
				} else {
					ctx.Error(http.StatusInternalServerError, "GetOwner", err)
				}
				return nil, nil, nil, nil, "", ""
			}
			headBranch = headInfos[1]
			headUser = headRepo.Owner
			isSameRepo = headRepo.ID == ctx.Repo.Repository.ID
		}
	} else {
		ctx.NotFound("CompareAndPullRequest", nil)
		return nil, nil, nil, nil, "", ""
	}
	ctx.Data["HeadUser"] = headUser
	ctx.Data["HeadBranch"] = headBranch
	ctx.Repo.PullRequest.SameRepo = isSameRepo
	if ctx.Repo.GitRepo == nil && !baseRepo.IsEmpty {
		var err error
		ctx.Repo.GitRepo, err = git.OpenRepository(ctx, ctx.Repo.Repository.RepoPath())
		if err != nil {
			ctx.Error(http.StatusInternalServerError, "OpenRepository", err)
			return nil, nil, nil, nil, "", ""
		}
		defer ctx.Repo.GitRepo.Close()
	}

	baseIsCommit := ctx.Repo.GitRepo.IsCommitExist(baseBranch)
	baseIsBranch := ctx.Repo.GitRepo.IsBranchExist(baseBranch)
	baseIsTag := ctx.Repo.GitRepo.IsTagExist(baseBranch)
	if !baseIsCommit && !baseIsBranch && !baseIsTag {
		if baseCommit, _ := ctx.Repo.GitRepo.GetCommit(baseBranch); baseCommit == nil {
			ctx.NotFound("IsRefExist", nil)
			return nil, nil, nil, nil, "", ""
		} else {
			baseBranch = baseCommit.ID.String()
			ctx.Data["BaseBranch"] = baseBranch
			baseIsCommit = true
		}
	}
	ctx.Data["BaseIsCommit"] = baseIsCommit
	ctx.Data["BaseIsBranch"] = baseIsBranch
	ctx.Data["BaseIsTag"] = baseIsTag

	var rootRepo *repo_model.Repository
	if baseRepo.IsFork {
		err = baseRepo.GetBaseRepo()
		if err != nil {
			if !repo_model.IsErrRepoNotExist(err) {
				ctx.NotFound("GetBaseRepo", nil)
			} else {
				ctx.Error(http.StatusInternalServerError, "GetBaseRepo", err)
			}
			return nil, nil, nil, nil, "", ""
		} else {
			rootRepo = baseRepo.BaseRepo
		}
	}

	var ownForkRpo *repo_model.Repository
	if ctx.Doer != nil && baseRepo.OwnerID != ctx.Doer.ID {
		has := repo_model.HasForkedRepo(ctx.Doer.ID, baseRepo.ID)
		if has {
			ownForkRpo = repo_model.GetForkedRepo(ctx.Doer.ID, baseRepo.ID)
			ctx.Data["OwnForkRepo"] = ownForkRpo
		}
	}

	has := headRepo != nil
	if !has && rootRepo != nil && rootRepo.OwnerID == headUser.ID {
		headRepo = rootRepo
		has = true
	}

	if !has && ownForkRpo != nil && ownForkRpo.OwnerID == headUser.ID {
		headRepo = ownForkRpo
		has = true
	}

	if !has {
		has = repo_model.HasForkedRepo(headUser.ID, baseRepo.ID)
		headRepo = repo_model.GetForkedRepo(headUser.ID, baseRepo.ID)
	}

	if !has && baseRepo.IsFork {
		has = repo_model.HasForkedRepo(headUser.ID, baseRepo.ForkID)
		headRepo = repo_model.GetForkedRepo(headUser.ID, baseRepo.ForkID)
	}

	if !isSameRepo && !has {
		ctx.Data["PageIsComparePull"] = false
	}

	var headGitRepo *gitea_git.Repository
	if isSameRepo {
		headRepo = ctx.Repo.Repository
		headGitRepo = ctx.Repo.GitRepo
	} else if has {
		headGitRepo, err = git.OpenRepository(ctx, headRepo.RepoPath())
		if err != nil {
			ctx.Error(http.StatusInternalServerError, "OpenRepository", err)
			return nil, nil, nil, nil, "", ""
		}
		defer headGitRepo.Close()
	}
	ctx.Data["HeadRepo"] = headRepo

	permBase, err := access_model.GetUserRepoPermission(ctx, baseRepo, ctx.Doer)
	if err != nil {
		ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err)
		return nil, nil, nil, nil, "", ""
	}

	if !permBase.CanRead(unit_model.TypeCode) {
		if log.IsTrace() {
			log.Trace("Permission Denied: User: %-v cannot read code in Repo: %-v\nUser in baseRepo has Permissions: %-+v",
				ctx.Doer,
				baseRepo,
				permBase)
		}
		ctx.NotFound("permBase.CanRead", nil)
		return nil, nil, nil, nil, "", ""
	}

	if !isSameRepo {
		permHead, err := access_model.GetUserRepoPermission(ctx, headRepo, ctx.Doer)
		if err != nil {
			ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err)
			return nil, nil, nil, nil, "", ""
		}
		if !permHead.CanRead(unit_model.TypeCode) {
			if log.IsTrace() {
				log.Trace("Permission Denied: User: %-v cannot read code in Repo: %-v\nUser in headRepo has Permissions: %-+v",
					ctx.Doer,
					headRepo,
					permHead)
			}
			ctx.NotFound("perHead.CanRead", nil)
			return nil, nil, nil, nil, "", ""
		}
	}

	if rootRepo != nil &&
		rootRepo.ID != headRepo.ID &&
		rootRepo.ID != baseRepo.ID {
		perm, branches, err := getBranchesForRepo(ctx, ctx.Doer, rootRepo)
		if err != nil {
			ctx.Error(http.StatusInternalServerError, "getBranchesForRepo", err)
			return nil, nil, nil, nil, "", ""
		}
		if perm {
			ctx.Data["RootRepo"] = rootRepo
			ctx.Data["RootRepoBranches"] = branches
		}
	}

	if ownForkRpo != nil &&
		ownForkRpo.ID != headRepo.ID &&
		ownForkRpo.ID != baseRepo.ID &&
		(rootRepo == nil || ownForkRpo.ID != rootRepo.ID) {
		perm, branches, err := getBranchesForRepo(ctx, ctx.Doer, ownForkRpo)
		if err != nil {
			ctx.Error(http.StatusInternalServerError, "getBranchesForRepo", err)
			return nil, nil, nil, nil, "", ""
		}
		if perm {
			ctx.Data["OwnForkRepo"] = ownForkRpo
			ctx.Data["OwnForkRepoBranches"] = branches
		}
	}

	headIsCommit := headGitRepo.IsCommitExist(headBranch)
	headIsBranch := headGitRepo.IsBranchExist(headBranch)
	headIsTag := headGitRepo.IsTagExist(headBranch)
	if !headIsCommit && !headIsBranch && !headIsTag {
		if headCommit, _ := headGitRepo.GetCommit(headBranch); headCommit == nil {
			ctx.NotFound("isRefExist", nil)
			return nil, nil, nil, nil, "", ""
		} else {
			headBranch = headCommit.ID.String()
			ctx.Data["HeadBranch"] = headBranch
			headIsCommit = true
		}
	}

	ctx.Data["HeadIsCommit"] = headIsCommit
	ctx.Data["HeadIsBranch"] = headIsBranch
	ctx.Data["HeadIsTag"] = headIsTag

	if ctx.Data["PageIsComparePull"] == nil {
		ctx.Data["PageIsComparePull"] = headIsBranch && baseIsBranch
	}

	if ctx.Data["PageIsComparePull"] == true && !permBase.CanReadIssuesOrPulls(true) {
		if log.IsTrace() {
			log.Trace("Permission Denied: User: %-v cannot create/read pull requests in Repo: %-v\nUser in baseRepo has Permissions: %-+v",
				ctx.Doer,
				baseRepo,
				permBase)
		}
		ctx.NotFound("can not read issues or pulls", nil)
		return nil, nil, nil, nil, "", ""
	}

	baseBranchRef := baseBranch
	if baseIsBranch {
		baseBranchRef = git.BranchPrefix + baseBranch
	} else if baseIsTag {
		baseBranchRef = git.TagPrefix + baseBranch
	}
	headBranchRef := headBranch
	if headIsBranch {
		headBranchRef = git.BranchPrefix + headBranch
	} else if headIsTag {
		headBranchRef = git.TagPrefix + headBranch
	}

	compareInfo, err := headGitRepo.GetCompareInfo(baseRepo.RepoPath(), baseBranchRef, headBranchRef, false, false)
	if err != nil {
		ctx.Error(http.StatusInternalServerError, "GetCompareInfo", err)
		return nil, nil, nil, nil, "", ""
	}
	ctx.Data["BeforeCommitID"] = compareInfo.MergeBase
	return headUser, headRepo, headGitRepo, compareInfo, baseBranch, headBranch
}

func getBranchesForRepo(ctx *context.APIContext, user *user_model.User, repo *repo_model.Repository) (bool, []string, error) {
	perm, err := access_model.GetUserRepoPermission(ctx, repo, user)
	if err != nil {
		return false, nil, err
	}
	if !perm.CanRead(unit_model.TypeCode) {
		return false, nil, nil
	}
	gitRepo, err := git.OpenRepository(ctx, repo.RepoPath())
	if err != nil {
		return false, nil, err
	}
	defer gitRepo.Close()
	branches, _, err := gitRepo.GetBranchNames(0, 0)
	if err != nil {
		return false, nil, err
	}
	return true, branches, nil
}

func MustBeNotEmpty(ctx *context.Context) {
	if ctx.Repo.Repository.IsEmpty {
		ctx.NotFound("MustBeNotEmpty", nil)
	}
}

func SetEditorconfigIfExists(ctx *context.Context) {
	if ctx.Repo.Repository.IsEmpty {
		ctx.Data["Edidorconfig"] = nil
		return
	}
	ec, err := ctx.Repo.GetEditorconfig()

	if err != nil && !git.IsErrNotExist(err) {
		description := fmt.Sprintf("Error while getting .Editconfig file: %v", err)
		if err := system_model.CreateRepositoryNotice(description); err != nil {
			ctx.ServerError("CreateRepositoryNotice", err)
		}
		return
	}
	ctx.Data["Editorconfig"] = ec
}

// SetdiffViewStyle set diff style as render variable
func SetDiffViewStyle(ctx *context.Context) {

	queryStyle := ctx.Params("style")
	if !ctx.IsSigned {
		ctx.Data["IsSplitStyle"] = queryStyle == "split"
		return
	}

	var (
		userStyle = ctx.Doer.DiffViewStyle
		style     string
	)

	if queryStyle == "unified" || queryStyle == "split" {
		style = queryStyle
	} else if userStyle == "unified" || userStyle == "split" {
		style = userStyle
	} else {
		style = "unified"
	}
	ctx.Data["IsSplitStyle"] = style == "split"
	if err := user_model.UpdateUserDiffViewStyle(ctx.Doer, style); err != nil {
		ctx.ServerError("ErrUpdateDiffViewStyle", err)
	}
}

func GetContributors(ctx *context.APIContext) {
	list, err := hat_activities_models.GetContributors(ctx.Repo.Repository.ID, ctx.Repo.Owner.ID)
	if err != nil {
		ctx.Error(http.StatusInternalServerError, "GetContributors", err)
		return
	}

	ctx.JSON(http.StatusOK, list)
}

type CountDTO struct {
	Branch       CountDTOBranch `json:"branch"`
	ReleaseCount int64          `json:"release_count"`
	TagCount     int64          `json:"tag_count"`
	BranchCount  int64          `json:"branch_count"`
}

type CountDTOBranch struct {
	CommitCount int64  `json:"commit_count"`
	BranchName  string `json:"branch_name"`
}

func GetCommitCount(ctx *context.APIContext) {
	ref := ctx.FormString("ref")
	if ref == "" {
		ref = ctx.Repo.Repository.DefaultBranch
	}

	commit, err := ctx.Repo.GitRepo.GetCommit(ref)
	if err != nil {
		ctx.Error(http.StatusInternalServerError, "GetCommit", err)
		return
	}

	commitCount, err := commit.CommitsCount()
	if err != nil {
		ctx.Error(http.StatusInternalServerError, "CommitsCount", err)
		return
	}

	releaseCount, err := repo_model.GetReleaseCountByRepoID(ctx.Repo.Repository.ID, repo_model.FindReleasesOptions{IncludeDrafts: true, IncludeTags: true})
	if err != nil {
		ctx.Error(http.StatusInternalServerError, "GetReleaseCountByRepoID", err)
		return
	}

	branches, _, err := ctx.Repo.GitRepo.GetBranchNames(0, 0)
	if err != nil {
		ctx.Error(http.StatusInternalServerError, "GetBranchNames", err)
		return
	}

	tags, err := ctx.Repo.GitRepo.GetTags(0, 0)
	if err != nil {
		ctx.Error(http.StatusInternalServerError, "GetTags", err)
		return
	}

	ctx.JSON(http.StatusOK, CountDTO{
		Branch: CountDTOBranch{
			CommitCount: commitCount,
			BranchName:  ref,
		},
		BranchCount:  int64(len(branches)),
		TagCount:     int64(len(tags)),
		ReleaseCount: releaseCount,
	})

}
