package repo

import (
	"fmt"
	"math"
	"net/http"
	"strconv"

	"code.gitea.io/gitea/models/repo"
	user "code.gitea.io/gitea/models/user"
	user_model "code.gitea.io/gitea/models/user"
	"code.gitea.io/gitea/modules/context"
	"code.gitea.io/gitea/modules/git"
	"code.gitea.io/gitea/modules/setting"
	api "code.gitea.io/gitea/modules/structs"
	"code.gitea.io/gitea/routers/api/v1/utils"
	"code.gitea.io/gitea/services/convert"
	"code.gitea.io/gitea/services/gitdiff"
	hat_convert "code.gitlink.org.cn/Gitlink/gitea_hat.git/modules/convert"
	hat_git "code.gitlink.org.cn/Gitlink/gitea_hat.git/modules/git"
	hat_api "code.gitlink.org.cn/Gitlink/gitea_hat.git/modules/structs"
)

func GetAllCommitsSliceByTime(ctx *context.APIContext) {
	if ctx.Repo.Repository.IsEmpty {
		ctx.JSON(http.StatusConflict, api.APIError{
			Message: "Git Repository is empty.",
			URL:     setting.API.SwaggerURL,
		})
		return
	}

	gitRepo, err := git.OpenRepository(ctx, ctx.Repo.Repository.RepoPath())
	if err != nil {
		ctx.Error(http.StatusInternalServerError, "OpenRepository", err)
		return
	}

	defer gitRepo.Close()

	listOptions := utils.GetListOptions(ctx)
	if listOptions.Page <= 0 {
		listOptions.Page = 1
	}

	if listOptions.PageSize > setting.Git.CommitsRangeSize {
		listOptions.PageSize = setting.Git.CommitsRangeSize
	}
	sha := ctx.FormString("sha")
	// path := ctx.FormString("path")
	not := ctx.FormString("not")
	var baseCommit *git.Commit
	if len(sha) == 0 {
		head, err := gitRepo.GetHEADBranch()
		if err != nil {
			ctx.Error(http.StatusInternalServerError, "GetHEADBranch", err)
			return
		}

		baseCommit, err = gitRepo.GetBranchCommit(head.Name)
		if err != nil {
			ctx.Error(http.StatusInternalServerError, "GetBranchCommit", err)
			return
		}

	} else {
		baseCommit, err = gitRepo.GetCommit(sha)
		if err != nil {
			ctx.Error(http.StatusInternalServerError, "GetCommit", err)
			return
		}
	}

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

	pageCount := int(math.Ceil(float64(commitsCountTotal)))

	commits, err := baseCommit.CommitsByRange(listOptions.Page, listOptions.PageSize, not)
	if err != nil {
		ctx.Error(http.StatusInternalServerError, "CommitsByRange", err)
		return
	}

	userCache := make(map[string]*user.User)

	apiCommits := make([]*responseCommit, len(commits))
	apiCommitsList := []responseCommit{}
	for i, commitPoniter := range commits {
		apiCommits[i], err = toResponseCommit(ctx, ctx.Repo.Repository, gitRepo, commitPoniter, userCache, convert.ParseCommitOptions(ctx))
		if err != nil {
			ctx.Error(http.StatusInternalServerError, "ToCommit", err)
			return
		}
		apiCommitsList = append(apiCommitsList, *apiCommits[i])
	}

	ctx.RespHeader().Set("X-Page", strconv.Itoa(listOptions.Page))
	ctx.RespHeader().Set("X-PerPage", strconv.Itoa(listOptions.PageSize))
	ctx.RespHeader().Set("X-Total", strconv.FormatInt(commitsCountTotal, 10))
	ctx.RespHeader().Set("X-PageCount", strconv.Itoa(pageCount))
	ctx.RespHeader().Set("X-HasMore", strconv.FormatBool(listOptions.Page < pageCount))

	ctx.SetLinkHeader(int(commitsCountTotal), listOptions.PageSize)
	ctx.RespHeader().Set("X-Total-Count", fmt.Sprintf("%d", commitsCountTotal))
	ctx.RespHeader().Set("Access-Control-Expose-Headers", "X-Total-Count, X-PerPage, X-Total, X-PageCount, X-HasMore, Link")

	ctx.JSON(http.StatusOK, apiCommitsList)
}

type responseCommit struct {
	*api.Commit
	CommitDate string `json:"commit_date"`
}

func toResponseCommit(ctx *context.APIContext, repo *repo.Repository, gitRepo *git.Repository, commit *git.Commit, userCache map[string]*user.User, opts convert.ToCommitOptions) (*responseCommit, error) {
	apiCommit, err := convert.ToCommit(ctx, repo, gitRepo, commit, userCache, opts)
	if err != nil {
		return nil, err
	}

	return &responseCommit{
		Commit:     apiCommit,
		CommitDate: apiCommit.Created.Format("2006-01-02"),
	}, nil
}

func GetRecentCommits(ctx *context.APIContext) {
	if ctx.Repo.Repository.IsEmpty {
		ctx.JSON(http.StatusConflict, api.APIError{
			Message: "Git Repository is empty",
			URL:     setting.API.SwaggerURL,
		})
		return
	}

	keyword := ctx.FormString("keyword")

	listOptions := utils.GetListOptions(ctx)
	if listOptions.Page <= 0 {
		listOptions.Page = 1
	}
	if listOptions.PageSize > setting.Git.CommitsRangeSize {
		listOptions.PageSize = setting.Git.CommitsRangeSize
	}

	var baseCommit *git.Commit
	var commitsCountTotal int64
	var err error
	head, err := ctx.Repo.GitRepo.GetHEADBranch()
	if err != nil {
		ctx.Error(http.StatusInternalServerError, "GetHeadBranch", err)
		return
	}
	baseCommit, err = ctx.Repo.GitRepo.GetBranchCommit(head.Name)
	if err != nil {
		ctx.Error(http.StatusInternalServerError, "GetBranchCommit", err)
		return
	}
	commitsCountTotal, err = hat_git.GetAllCommitsCount(ctx.Repo.GitRepo, keyword)
	if err != nil {
		ctx.Error(http.StatusInternalServerError, "CommitsCountFiles", err)
		return
	}
	pageCount := int(math.Ceil(float64(commitsCountTotal) / float64(listOptions.PageSize)))
	commits, err := hat_git.AllCommitsByFileAndRange(ctx.Repo.GitRepo,
		git.CommitsByFileAndRangeOptions{
			Revision: baseCommit.ID.String(),
			File:     ".",
			Page:     listOptions.Page,
		}, keyword, listOptions.PageSize)
	if err != nil {
		ctx.Error(http.StatusInternalServerError, "CommitsByRange", err)
		return
	}

	userCache := make(map[string]*user_model.User)
	apiCommits := make([]*api.Commit, len(commits))
	for i, commit := range commits {
		apiCommits[i], err = convert.ToCommit(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, commit, userCache, convert.ParseCommitOptions(ctx))
		if err != nil {
			ctx.Error(http.StatusInternalServerError, "ToCommit", err)
			return
		}
	}

	ctx.Resp.Header().Set("X-Page", strconv.Itoa(listOptions.Page))
	ctx.Resp.Header().Set("X-PerPage", strconv.Itoa(listOptions.PageSize))
	ctx.Resp.Header().Set("X-Total", strconv.FormatInt(commitsCountTotal, 10))
	ctx.Resp.Header().Set("X-PageCount", strconv.Itoa(pageCount))
	ctx.Resp.Header().Set("X-HasMore", strconv.FormatBool(listOptions.Page < pageCount))

	ctx.SetLinkHeader(int(commitsCountTotal), listOptions.PageSize)
	ctx.Resp.Header().Set("X-Total-Count", fmt.Sprintf("%d", commitsCountTotal))

	ctx.JSON(http.StatusOK, apiCommits)
}

func GetFileAllCommits(ctx *context.APIContext) {
	if ctx.Repo.Repository.IsEmpty {
		ctx.JSON(http.StatusConflict, api.APIError{
			Message: "Git Repository is empty",
			URL:     setting.API.SwaggerURL,
		})
		return
	}
	listOptions := utils.GetListOptions(ctx)
	if listOptions.Page <= 0 {
		listOptions.Page = 1
	}
	if listOptions.PageSize > setting.Git.CommitsRangeSize {
		listOptions.PageSize = setting.Git.CommitsRangeSize
	}

	ref := ctx.FormString("sha")
	if ref == "" {
		ref = ctx.FormString("ref")
	}

	treePath := ctx.Params("*")
	var baseCommit *git.Commit
	var commitsCountTotal int64
	var err error
	if len(ref) == 0 {
		head, err := ctx.Repo.GitRepo.GetHEADBranch()
		if err != nil {
			ctx.Error(http.StatusInternalServerError, "GetHeadBranch", err)
			return
		}
		baseCommit, err = ctx.Repo.GitRepo.GetBranchCommit(head.Name)
		if err != nil {
			ctx.Error(http.StatusInternalServerError, "GetBranchCommit", err)
			return
		}
		commitsCountTotal, err = ctx.Repo.GitRepo.FileCommitsCount(head.Name, treePath)
		if err != nil {
			ctx.Error(http.StatusInternalServerError, "CommitsCountFiles", err)
			return
		}

	} else {
		baseCommit, err = ctx.Repo.GitRepo.GetCommit(ref)
		if err != nil {
			ctx.Error(http.StatusInternalServerError, "GetCommit", err)
			return
		}
		commitsCountTotal, err = ctx.Repo.GitRepo.FileCommitsCount(ref, treePath)
		if err != nil {
			ctx.Error(http.StatusInternalServerError, "CommitsCountFiles", err)
			return
		}

	}
	pageCount := int(math.Ceil(float64(commitsCountTotal) / float64(listOptions.PageSize)))
	commits, err := ctx.Repo.GitRepo.CommitsByFileAndRange(
		git.CommitsByFileAndRangeOptions{
			Revision: baseCommit.ID.String(),
			File:     treePath,
			Page:     listOptions.Page,
		})
	if err != nil {
		ctx.Error(http.StatusInternalServerError, "CommitsByRange", err)
		return
	}

	userCache := make(map[string]*user_model.User)
	apiCommits := make([]*api.Commit, len(commits))
	for i, commit := range commits {
		apiCommits[i], err = convert.ToCommit(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, commit, userCache, convert.ParseCommitOptions(ctx))
		if err != nil {
			ctx.Error(http.StatusInternalServerError, "ToCommit", err)
			return
		}
	}

	ctx.Resp.Header().Set("X-Page", strconv.Itoa(listOptions.Page))
	ctx.Resp.Header().Set("X-PerPage", strconv.Itoa(listOptions.PageSize))
	ctx.Resp.Header().Set("X-Total", strconv.FormatInt(commitsCountTotal, 10))
	ctx.Resp.Header().Set("X-PageCount", strconv.Itoa(pageCount))
	ctx.Resp.Header().Set("X-HasMore", strconv.FormatBool(listOptions.Page < pageCount))

	ctx.SetLinkHeader(int(commitsCountTotal), listOptions.PageSize)
	ctx.Resp.Header().Set("X-Total-Count", fmt.Sprintf("%d", commitsCountTotal))

	ctx.JSON(http.StatusOK, apiCommits)
}

func GetSingleCommit(ctx *context.APIContext) {
	sha := ctx.Params(":sha")
	if !git.IsValidRefPattern(sha) {
		ctx.Error(http.StatusUnprocessableEntity, "no valid ref or sha", fmt.Sprintf("no valid ref or sha: %s", sha))
		return
	}

	commit, err := ctx.Repo.GitRepo.GetCommit(sha)
	if err != nil {
		if git.IsErrNotExist(err) {
			ctx.NotFound(sha)
			return
		}
		ctx.Error(http.StatusInternalServerError, "gitRepo.GetCommit", err)
		return
	}

	json, err := hat_convert.ToCommit(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, commit, nil, convert.ParseCommitOptions(ctx))
	if err != nil {
		ctx.Error(http.StatusInternalServerError, "toCommit", err)
		return
	}
	ctx.JSON(http.StatusOK, json)
}

func GetCommitFiles(ctx *context.APIContext) {
	commitID := ctx.Params(":sha")
	gitRepo := ctx.Repo.GitRepo

	commit, err := gitRepo.GetCommit(commitID)
	if err != nil {
		if git.IsErrNotExist(err) {
			ctx.NotFound(commitID)
			return
		}
		ctx.Error(http.StatusInternalServerError, "gitRepo.GetCommit", err)
		return
	}

	if len(commitID) != 40 {
		commitID = commit.ID.String()
	}

	diff, err := gitdiff.GetDiff(gitRepo, &gitdiff.DiffOptions{
		AfterCommitID:      commitID,
		SkipTo:             ctx.FormString("skip-to"),
		MaxLines:           setting.Git.MaxGitDiffLines,
		MaxLineCharacters:  setting.Git.MaxGitDiffLineCharacters,
		MaxFiles:           -1,
		WhitespaceBehavior: gitdiff.GetWhitespaceFlag(ctx.FormString("whitespace")),
	})
	if err != nil {
		ctx.NotFound("GetDIff", err)
		return
	}
	listOptions := utils.GetListOptions(ctx)

	totalNumberOfFiles := diff.NumFiles
	totalNumberOfPages := int(math.Ceil(float64(totalNumberOfFiles) / float64(listOptions.PageSize)))

	start, end := listOptions.GetStartEnd()

	if end > totalNumberOfFiles {
		end = totalNumberOfFiles
	}

	lenFiles := end - start
	if lenFiles < 0 {
		lenFiles = 0
	}

	apiFiles := make([]*hat_api.ChangedFile, 0, lenFiles)
	for i := start; i < end; i++ {
		apiFiles = append(apiFiles, hat_convert.ToChangedFile(diff.Files[i], ctx.Repo.Repository, commitID))
	}
	ctx.SetLinkHeader(totalNumberOfFiles, listOptions.PageSize)
	ctx.SetTotalCountHeader(int64(totalNumberOfFiles))

	ctx.RespHeader().Set("X-Page", strconv.Itoa(listOptions.Page))
	ctx.RespHeader().Set("X-PerPage", strconv.Itoa(listOptions.PageSize))
	ctx.RespHeader().Set("X-PageCount", strconv.Itoa(totalNumberOfPages))
	ctx.RespHeader().Set("X-HasMore", strconv.FormatBool(listOptions.Page < totalNumberOfPages))
	ctx.AppendAccessControlExposeHeaders("X-Page", "X-PerPage", "X-PageCount", "X-HasMore")

	ctx.JSON(http.StatusOK, struct {
		Files         []*hat_api.ChangedFile `json:"files"`
		TotalAddition int                    `json:"total_addition"`
		TotalDeletion int                    `json:"total_deletion"`
	}{
		Files:         apiFiles,
		TotalAddition: diff.TotalAddition,
		TotalDeletion: diff.TotalDeletion,
	})
}

func GetCommitFilesByPath(ctx *context.APIContext) {
	commitID := ctx.Params(":sha")
	gitRepo := ctx.Repo.GitRepo
	path := ctx.Params("*")
	commit, err := gitRepo.GetCommit(commitID)
	if err != nil {
		if git.IsErrNotExist(err) {
			ctx.NotFound(commitID)
			return
		}
		ctx.Error(http.StatusInternalServerError, "gitRepo.GetCommit", err)
		return
	}

	if len(commitID) != 40 {
		commitID = commit.ID.String()
	}

	diff, err := gitdiff.GetDiff(gitRepo, &gitdiff.DiffOptions{
		AfterCommitID:      commitID,
		SkipTo:             ctx.FormString("skip-to"),
		MaxLines:           setting.Git.MaxGitDiffLines,
		MaxLineCharacters:  setting.Git.MaxGitDiffLineCharacters,
		MaxFiles:           -1,
		WhitespaceBehavior: gitdiff.GetWhitespaceFlag(ctx.FormString("whitespace")),
	}, path)
	if err != nil {
		ctx.NotFound("GetDIff", err)
		return
	}

	ctx.JSON(http.StatusOK, &diff)

}

func GetCommitDiff(ctx *context.APIContext) {
	commitID := ctx.Params(":sha")
	gitRepo := ctx.Repo.GitRepo

	commit, err := gitRepo.GetCommit(commitID)
	if err != nil {
		if git.IsErrNotExist(err) {
			ctx.NotFound(commitID)
			return
		}
		ctx.Error(http.StatusInternalServerError, "gitRepo.GetCommit", err)
		return
	}

	if len(commitID) != 40 {
		commitID = commit.ID.String()
	}

	diff, err := gitdiff.GetDiff(gitRepo, &gitdiff.DiffOptions{
		AfterCommitID:     commitID,
		MaxLines:          setting.Git.MaxGitDiffLines,
		MaxLineCharacters: setting.Git.MaxGitDiffLineCharacters,
		MaxFiles:          setting.Git.MaxGitDiffFiles,
	})
	if err != nil {
		ctx.NotFound("GetDIff", err)
		return
	}
	ctx.JSON(http.StatusOK, &diff)
}
