package repo

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

	"code.gitea.io/gitea/models"
	asymkey_model "code.gitea.io/gitea/models/asymkey"
	git_model "code.gitea.io/gitea/models/git"
	issues_model "code.gitea.io/gitea/models/issues"
	repo_model "code.gitea.io/gitea/models/repo"
	"code.gitea.io/gitea/models/unit"
	user_model "code.gitea.io/gitea/models/user"
	"code.gitea.io/gitea/services/gitdiff"

	"code.gitea.io/gitea/modules/context"
	gitea_convert "code.gitea.io/gitea/modules/convert"
	"code.gitea.io/gitea/modules/git"
	"code.gitea.io/gitea/modules/log"
	"code.gitea.io/gitea/modules/notification"
	"code.gitea.io/gitea/modules/setting"
	gitea_api "code.gitea.io/gitea/modules/structs"
	"code.gitea.io/gitea/modules/timeutil"
	"code.gitea.io/gitea/modules/web"
	issue_service "code.gitea.io/gitea/services/issue"
	pull_service "code.gitea.io/gitea/services/pull"
	"code.gitlink.org.cn/Gitlink/gitea_hat.git/modules/convert"
	hat_git "code.gitlink.org.cn/Gitlink/gitea_hat.git/modules/git"
	hat_pull_service "code.gitlink.org.cn/Gitlink/gitea_hat.git/services/pull"
)

func CreatePrVersion(ctx *context.APIContext) {
	form := web.GetForm(ctx).(*gitea_api.PullRequestPayload)
	hat_pull_service.AddToTaskQueue(&issues_model.PullRequest{ID: form.PullRequest.ID}, string(form.Action))
	ctx.Status(http.StatusNoContent)
}

func EditPullRequest(ctx *context.APIContext) {
	form := web.GetForm(ctx).(*gitea_api.EditPullRequestOption)
	pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
	if err != nil {
		if issues_model.IsErrPullRequestNotExist(err) {
			ctx.NotFound()
		} else {
			ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err)
		}
		return
	}

	err = pr.LoadIssue()
	if err != nil {
		ctx.Error(http.StatusInternalServerError, "LoadIssue", err)
		return
	}
	issue := pr.Issue
	issue.Repo = ctx.Repo.Repository

	if err := issue.LoadAttributes(ctx); err != nil {
		ctx.Error(http.StatusInternalServerError, "LoadAttributes", err)
		return
	}

	if !issue.IsPoster(ctx.Doer.ID) && !ctx.Repo.CanWrite(unit.TypePullRequests) {
		ctx.Status(http.StatusForbidden)
		return
	}

	oldTitle := issue.Title
	if len(form.Title) > 0 {
		issue.Title = form.Title
	}
	if len(form.Body) > 0 {
		issue.Content = form.Body
	}

	// Update or remove deadline if set
	if form.Deadline != nil || form.RemoveDeadline != nil {
		var deadlineUnix timeutil.TimeStamp
		if (form.RemoveDeadline == nil || !*form.RemoveDeadline) && !form.Deadline.IsZero() {
			deadline := time.Date(form.Deadline.Year(), form.Deadline.Month(), form.Deadline.Day(),
				23, 59, 59, 0, form.Deadline.Location())
			deadlineUnix = timeutil.TimeStamp(deadline.Unix())
		}

		if err := issues_model.UpdateIssueDeadline(issue, deadlineUnix, ctx.Doer); err != nil {
			ctx.Error(http.StatusInternalServerError, "UpdateIssueDeadline", err)
			return
		}
		issue.DeadlineUnix = deadlineUnix
	}

	// Add/delete assignees

	// Deleting is done the GitHub way (quote from their api documentation):
	// https://developer.github.com/v3/issues/#edit-an-issue
	// "assignees" (array): Logins for Users to assign to this issue.
	// Pass one or more user logins to replace the set of assignees on this Issue.
	// Send an empty array ([]) to clear all assignees from the Issue.

	if ctx.Repo.CanWrite(unit.TypePullRequests) && (form.Assignees != nil || len(form.Assignee) > 0) {
		err = issue_service.UpdateAssignees(issue, form.Assignee, form.Assignees, ctx.Doer)
		if err != nil {
			if user_model.IsErrUserNotExist(err) {
				ctx.Error(http.StatusUnprocessableEntity, "", fmt.Sprintf("Assignee does not exist: [name: %s]", err))
			} else {
				ctx.Error(http.StatusInternalServerError, "UpdateAssignees", err)
			}
			return
		}
	}

	if ctx.Repo.CanWrite(unit.TypePullRequests) && form.Milestone != 0 &&
		issue.MilestoneID != form.Milestone {
		oldMilestoneID := issue.MilestoneID
		issue.MilestoneID = form.Milestone
		if err = issue_service.ChangeMilestoneAssign(issue, ctx.Doer, oldMilestoneID); err != nil {
			ctx.Error(http.StatusInternalServerError, "ChangeMilestoneAssign", err)
			return
		}
	}

	if ctx.Repo.CanWrite(unit.TypePullRequests) && form.Labels != nil {
		labels, err := issues_model.GetLabelsInRepoByIDs(ctx.Repo.Repository.ID, form.Labels)
		if err != nil {
			ctx.Error(http.StatusInternalServerError, "GetLabelsInRepoByIDsError", err)
			return
		}

		if ctx.Repo.Owner.IsOrganization() {
			orgLabels, err := issues_model.GetLabelsInOrgByIDs(ctx.Repo.Owner.ID, form.Labels)
			if err != nil {
				ctx.Error(http.StatusInternalServerError, "GetLabelsInOrgByIDs", err)
				return
			}

			labels = append(labels, orgLabels...)
		}

		if err = issues_model.ReplaceIssueLabels(issue, labels, ctx.Doer); err != nil {
			ctx.Error(http.StatusInternalServerError, "ReplaceLabelsError", err)
			return
		}
	}

	if form.State != nil {
		if pr.HasMerged {
			ctx.Error(http.StatusPreconditionFailed, "MergedPRState", "cannot change state of this pull request, it was already merged")
			return
		}
		issue.IsClosed = gitea_api.StateClosed == gitea_api.StateType(*form.State)
	}
	statusChangeComment, titleChanged, err := issues_model.UpdateIssueByAPI(issue, ctx.Doer)
	if err != nil {
		if issues_model.IsErrDependenciesLeft(err) {
			ctx.Error(http.StatusPreconditionFailed, "DependenciesLeft", "cannot close this pull request because it still has open dependencies")
			return
		}
		ctx.Error(http.StatusInternalServerError, "UpdateIssueByAPI", err)
		return
	}

	if titleChanged {
		notification.NotifyIssueChangeTitle(ctx.Doer, issue, oldTitle)
	}

	if statusChangeComment != nil {
		notification.NotifyIssueChangeStatus(ctx.Doer, issue, statusChangeComment, issue.IsClosed)
	}

	// change pull target branch
	if !pr.HasMerged && len(form.Base) != 0 && form.Base != pr.BaseBranch {
		if !ctx.Repo.GitRepo.IsBranchExist(form.Base) {
			ctx.Error(http.StatusNotFound, "NewBaseBranchNotExist", fmt.Errorf("new base '%s' not exist", form.Base))
			return
		}
		if err := pull_service.ChangeTargetBranch(ctx, pr, ctx.Doer, form.Base); err != nil {
			if issues_model.IsErrPullRequestAlreadyExists(err) {
				ctx.Error(http.StatusConflict, "IsErrPullRequestAlreadyExists", err)
				return
			} else if issues_model.IsErrIssueIsClosed(err) {
				ctx.Error(http.StatusUnprocessableEntity, "IsErrIssueIsClosed", err)
				return
			} else if models.IsErrPullRequestHasMerged(err) {
				ctx.Error(http.StatusConflict, "IsErrPullRequestHasMerged", err)
				return
			} else {
				ctx.InternalServerError(err)
			}
			return
		}
		notification.NotifyPullRequestChangeTargetBranch(ctx.Doer, pr, form.Base)
	}

	// update allow edits
	if form.AllowMaintainerEdit != nil {
		if err := pull_service.SetAllowEdits(ctx, ctx.Doer, pr, *form.AllowMaintainerEdit); err != nil {
			if errors.Is(pull_service.ErrUserHasNoPermissionForAction, err) {
				ctx.Error(http.StatusForbidden, "SetAllowEdits", fmt.Sprintf("SetAllowEdits: %s", err))
				return
			}
			ctx.ServerError("SetAllowEdits", err)
			return
		}
	}

	// Refetch from database
	pr, err = issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, pr.Index)
	if err != nil {
		if issues_model.IsErrPullRequestNotExist(err) {
			ctx.NotFound()
		} else {
			ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err)
		}
		return
	}

	// TODO this should be 200, not 201
	ctx.JSON(http.StatusCreated, gitea_convert.ToAPIPullRequest(ctx, pr, ctx.Doer))
}

func GetPullRequest(ctx *context.APIContext) {
	pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
	if err != nil {
		if issues_model.IsErrPullRequestNotExist(err) {
			ctx.NotFound()
		} else {
			ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err)
		}
		return
	}

	if err = pr.LoadBaseRepoCtx(ctx); err != nil {
		ctx.Error(http.StatusInternalServerError, "LoadBaseRepo", err)
		return
	}
	if err = pr.LoadHeadRepoCtx(ctx); err != nil {
		ctx.Error(http.StatusInternalServerError, "LoadHeadRepo", err)
		return
	}
	apiResult, err := convert.ToAPIPullRequest(ctx, ctx.Repo.GitRepo, pr, ctx.Doer)
	if err != nil {
		ctx.Error(http.StatusInternalServerError, "ToAPIPullRequest", err)
		return
	}
	ctx.JSON(http.StatusOK, apiResult)
}

type PullRequestCommit struct {
	git_model.SignCommitWithStatuses
	Sha string
}

func GetPullCommits(ctx *context.APIContext) {
	pull, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
	if err != nil {
		if issues_model.IsErrPullRequestNotExist(err) {
			ctx.NotFound()
		} else {
			ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err)
		}
		return
	}

	issue := pull.Issue
	if issue == nil {
		ctx.NotFound("issue is not present.")
		return
	}
	gitRepo := ctx.Repo.GitRepo

	var commits []*git.Commit
	var prInfo *git.CompareInfo

	if pull.HasMerged {
		var baseCommit string
		if pull.MergeBase == "" {
			var commitSHA, parentCommit string
			commitSHA, err := gitRepo.GetRefCommitID(pull.GetGitRefName())
			if err != nil {
				commitSHA, err := gitRepo.ReadPatchCommit(pull.Index)
				if err == nil {
					if err := gitRepo.SetReference(pull.GetGitRefName(), commitSHA); err != nil {
						log.Error("Could not write head file", err)
					}
				} else {
					log.Trace("No history file available for PR %d", pull.Index)
				}
			}
			if commitSHA != "" {
				parentCommit, _, err = git.NewCommand(ctx, "rev-list", "-1", "--skip=1").AddDynamicArguments(commitSHA).RunStdString(&git.RunOpts{Dir: gitRepo.Path})
				if err == nil {
					parentCommit = strings.TrimSpace(parentCommit)
				}
				if err != nil || parentCommit == "" {
					log.Info("No known parent commit for PR %d, error: %v", pull.Index, err)
					parentCommit = commitSHA
				}
			}
			baseCommit = parentCommit
		} else {
			baseCommit = pull.MergeBase
		}
		prInfo, err = gitRepo.GetCompareInfo(gitRepo.Path, baseCommit, pull.GetGitRefName(), false, false)

	} else {
		prInfo, err = gitRepo.GetCompareInfo(gitRepo.Path, pull.MergeBase, pull.GetGitRefName(), false, false)
	}

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

	ctx.Data["Username"] = ctx.Repo.Owner.Name
	ctx.Data["Reponame"] = ctx.Repo.Repository.Name
	commits = prInfo.Commits
	userCommits := user_model.ValidateCommitsWithEmails(commits)
	signCommits := asymkey_model.ParseCommitsWithSignature(userCommits, ctx.Repo.Repository.GetTrustModel(),
		func(user *user_model.User) (bool, error) {
			return repo_model.IsOwnerMemberCollaborator(ctx.Repo.Repository, user.ID)
		})
	signStatusCommits := git_model.ParseCommitsWithStatus(signCommits, ctx.Repo.Repository)

	result := make([]PullRequestCommit, 0)
	for _, commit := range signStatusCommits {
		result = append(result, PullRequestCommit{
			SignCommitWithStatuses: *commit,
			Sha:                    commit.ID.String(),
		})
	}

	ctx.JSON(http.StatusOK, result)
}

func GetPullFiles(ctx *context.APIContext) {
	pull, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
	if err != nil {
		if issues_model.IsErrPullRequestNotExist(err) {
			ctx.NotFound()
		} else {
			ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err)
		}
		return
	}

	issue := pull.Issue
	if issue == nil {
		ctx.NotFound("issue is not present.")
		return
	}
	gitRepo := ctx.Repo.GitRepo

	var prInfo *git.CompareInfo

	if pull.HasMerged {
		var baseCommit string
		if pull.MergeBase == "" {
			var commitSHA, parentCommit string
			commitSHA, err := gitRepo.GetRefCommitID(pull.GetGitRefName())
			if err != nil {
				commitSHA, err := gitRepo.ReadPatchCommit(pull.Index)
				if err == nil {
					if err := gitRepo.SetReference(pull.GetGitRefName(), commitSHA); err != nil {
						log.Error("Could not write head file", err)
					}
				} else {
					log.Trace("No history file available for PR %d", pull.Index)
				}
			}
			if commitSHA != "" {
				parentCommit, _, err = git.NewCommand(ctx, "rev-list", "-1", "--skip=1").AddDynamicArguments(commitSHA).RunStdString(&git.RunOpts{Dir: gitRepo.Path})
				if err == nil {
					parentCommit = strings.TrimSpace(parentCommit)
				}
				if err != nil || parentCommit == "" {
					log.Info("No known parent commit for PR %d, error: %v", pull.Index, err)
					parentCommit = commitSHA
				}
			}
			baseCommit = parentCommit
		} else {
			baseCommit = pull.MergeBase
		}
		prInfo, err = gitRepo.GetCompareInfo(gitRepo.Path, baseCommit, pull.GetGitRefName(), false, false)

	} else {
		prInfo, err = gitRepo.GetCompareInfo(gitRepo.Path, pull.MergeBase, pull.GetGitRefName(), false, false)
	}

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

	headCommitID, err := gitRepo.GetRefCommitID(pull.GetGitRefName())
	if err != nil {
		ctx.Error(http.StatusInternalServerError, "GetRefCommitID", err)
		return
	}

	startCommitID := prInfo.MergeBase
	endCommitID := headCommitID

	ctx.Data["WhitespaceBehavior"] = ""
	diff := &gitdiff.Diff{Files: make([]*gitdiff.DiffFile, 0)}
	if ctx.Params("not-need-files") == "true" || ctx.Params("only-file-name") == "true" {
		// shortstatArgs := []string{startCommitID + "..." + endCommitID}
		shortStatArgs := []git.CmdArg{git.CmdArgCheck(startCommitID + "..." + endCommitID)}
		if len(startCommitID) == 0 || startCommitID == git.EmptySHA {
			shortStatArgs = []git.CmdArg{git.EmptyTreeSHA, git.CmdArgCheck(endCommitID)}
		}
		diff.NumFiles, diff.TotalAddition, diff.TotalDeletion, err = git.GetDiffShortStat(ctx, gitRepo.Path, shortStatArgs...)
		if err != nil && strings.Contains(err.Error(), "no merge base") {
			// git >= 2.28 now returns an error if base and head have become unrelated.
			// previously it would return the results of git diff --shortstat base head so let's try that...
			shortStatArgs = []git.CmdArg{git.CmdArgCheck(startCommitID), git.CmdArgCheck(endCommitID)}
			diff.NumFiles, diff.TotalAddition, diff.TotalDeletion, _ = git.GetDiffShortStat(ctx, gitRepo.Path, shortStatArgs...)
		}
		if ctx.Params("only-file-name") == "true" {
			var diffString string
			var err error
			if len(startCommitID) == 0 || startCommitID == git.EmptySHA {
				diffString, err = hat_git.GetDiffFileOnlyName(ctx.Repo.GitRepo, git.EmptyTreeSHA, headCommitID)
			} else {
				diffString, err = hat_git.GetDiffFileOnlyName(ctx.Repo.GitRepo, startCommitID, headCommitID)
			}
			if err == nil {
				for _, fileName := range strings.Split(diffString, "\n") {
					if fileName != "" {
						diff.Files = append(diff.Files, &gitdiff.DiffFile{
							Name: fileName,
						})
					}
				}
			}
		}
	} else {
		diff, err = gitdiff.GetDiff(gitRepo, &gitdiff.DiffOptions{
			BeforeCommitID:     startCommitID,
			AfterCommitID:      endCommitID,
			SkipTo:             ctx.FormString("skip-to"),
			MaxLines:           setting.Git.MaxGitDiffLines,
			MaxLineCharacters:  setting.Git.MaxGitDiffLineCharacters,
			MaxFiles:           setting.Git.MaxGitDiffFiles,
			WhitespaceBehavior: gitdiff.GetWhitespaceFlag(ctx.Data["WhitespaceBehavior"].(string)),
		})
		if err != nil {
			ctx.ServerError("GetDiffRangeWithWhitespaceBehavior", err)
			return
		}

		if err = diff.LoadComments(ctx, issue, ctx.Doer); err != nil {
			ctx.ServerError("LoadComments", err)
			return
		}
	}

	fileDiff := struct {
		*gitdiff.Diff
		LatestSha string
	}{
		Diff:      diff,
		LatestSha: endCommitID,
	}

	ctx.JSON(http.StatusOK, fileDiff)
}
